CrackedRuby logo

CrackedRuby

yield and block_given?

Overview

Ruby provides yield and block_given? as core language features for method-block interaction. The yield keyword executes a block passed to a method, while block_given? determines whether a block was provided during method invocation.

When Ruby encounters yield in a method body, it transfers control to the block associated with the current method call. The block executes with any arguments passed through yield, then returns control to the method. If no block exists when yield executes, Ruby raises a LocalJumpError.

The block_given? method returns true when a block accompanies the method call, false otherwise. This conditional check prevents errors when yield might execute without an available block.

def greet(name)
  puts "Hello, #{name}!"
  yield if block_given?
end

greet("Alice") { puts "Nice to meet you!" }
# Hello, Alice!
# Nice to meet you!

greet("Bob")
# Hello, Bob!

Ruby treats blocks as anonymous functions that methods can invoke multiple times or conditionally. The block maintains access to variables from its defining scope through closure behavior.

def repeat(times)
  times.times { |i| yield(i) }
end

counter = 0
repeat(3) do |iteration|
  counter += iteration
  puts "Iteration #{iteration}, counter: #{counter}"
end
# Iteration 0, counter: 0
# Iteration 1, counter: 1
# Iteration 2, counter: 3

The yield mechanism forms the foundation for Ruby's iteration patterns, custom control structures, and callback systems. Methods can pass multiple arguments to blocks, collect return values, and handle block exceptions within the method body.

Basic Usage

Ruby methods accept blocks implicitly without declaring them in the parameter list. The yield keyword executes the block with optional arguments, while block_given? provides safe conditional execution.

def calculate(x, y)
  result = x + y
  if block_given?
    yield(result)
  else
    result
  end
end

# With block
calculate(5, 3) { |sum| sum * 2 }
# => 16

# Without block
calculate(5, 3)
# => 8

Methods can yield multiple times during execution, creating iterator-like behavior. Each yield call transfers control to the block with fresh arguments.

def each_word(sentence)
  sentence.split.each { |word| yield(word) }
end

each_word("Ruby blocks are flexible") do |word|
  puts word.upcase
end
# RUBY
# BLOCKS
# ARE
# FLEXIBLE

The block's return value becomes the value of the yield expression within the method. This allows methods to collect and process block results.

def transform_numbers(*numbers)
  results = []
  numbers.each do |num|
    transformed = yield(num)
    results << transformed
  end
  results
end

doubled = transform_numbers(1, 2, 3, 4) { |n| n * 2 }
# => [2, 4, 6, 8]

squared = transform_numbers(1, 2, 3, 4) { |n| n ** 2 }
# => [1, 4, 9, 16]

Ruby blocks can accept multiple parameters through yield, enabling complex data passing between method and block.

def process_pairs(hash)
  hash.each do |key, value|
    yield(key, value)
  end
end

data = { name: "Alice", age: 30, city: "Portland" }
process_pairs(data) do |attribute, info|
  puts "#{attribute.to_s.capitalize}: #{info}"
end
# Name: Alice
# Age: 30
# City: Portland

The block_given? method enables methods to provide different behavior based on block presence, creating flexible APIs that work with or without blocks.

def log_message(message, level = :info)
  timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")

  if block_given?
    formatted = yield(message, level, timestamp)
    puts formatted
  else
    puts "[#{timestamp}] #{level.to_s.upcase}: #{message}"
  end
end

log_message("System started")
# [2024-01-15 10:30:45] INFO: System started

log_message("Database error", :error) do |msg, lvl, time|
  "💥 [#{time}] #{msg} (#{lvl})"
end
# 💥 [2024-01-15 10:30:45] Database error (error)

Advanced Usage

Ruby's yield supports sophisticated control flow patterns, including early termination, exception handling, and nested block structures. Methods can manage block execution context and implement custom iteration protocols.

def retry_operation(max_attempts = 3)
  attempts = 0
  begin
    attempts += 1
    result = yield(attempts)
    return result
  rescue StandardError => e
    if attempts < max_attempts
      puts "Attempt #{attempts} failed: #{e.message}. Retrying..."
      sleep(0.1 * attempts)  # Exponential backoff
      retry
    else
      raise e
    end
  end
end

# Simulate unreliable operation
result = retry_operation(4) do |attempt_num|
  if attempt_num < 3
    raise "Connection timeout"
  else
    "Success on attempt #{attempt_num}"
  end
end
# Attempt 1 failed: Connection timeout. Retrying...
# Attempt 2 failed: Connection timeout. Retrying...
# => "Success on attempt 3"

Methods can implement complex control structures using yield with conditional logic and state management. This enables domain-specific languages and custom iteration patterns.

def transaction
  @in_transaction = true
  @changes = []

  begin
    yield(self)
    commit_changes
  rescue StandardError => e
    rollback_changes
    raise e
  ensure
    @in_transaction = false
  end
end

def add_record(data)
  if @in_transaction
    @changes << [:add, data]
  else
    raise "Not in transaction"
  end
end

def commit_changes
  @changes.each { |action, data| puts "Committing: #{action} #{data}" }
  @changes.clear
end

def rollback_changes
  puts "Rolling back #{@changes.size} changes"
  @changes.clear
end

transaction do |tx|
  tx.add_record(name: "Alice")
  tx.add_record(name: "Bob")
  # All changes committed together
end

Ruby blocks can return complex data structures that methods process for advanced functionality. This pattern supports builder patterns and configuration systems.

def build_query
  @conditions = []
  @joins = []
  @order = nil

  yield(self) if block_given?

  query_parts = ["SELECT * FROM users"]
  query_parts += @joins unless @joins.empty?
  query_parts << "WHERE #{@conditions.join(' AND ')}" unless @conditions.empty?
  query_parts << "ORDER BY #{@order}" if @order

  query_parts.join(" ")
end

def where(condition)
  @conditions << condition
  self
end

def join(table)
  @joins << "JOIN #{table}"
  self
end

def order_by(column)
  @order = column
  self
end

query = build_query do |q|
  q.where("age > 18")
   .where("active = true")
   .join("profiles ON users.id = profiles.user_id")
   .order_by("created_at DESC")
end
# => "SELECT * FROM users JOIN profiles ON users.id = profiles.user_id WHERE age > 18 AND active = true ORDER BY created_at DESC"

Methods can capture blocks as Proc objects for deferred execution or storage, enabling callback registration and event systems.

class EventDispatcher
  def initialize
    @listeners = {}
  end

  def on(event_type, &block)
    @listeners[event_type] ||= []
    @listeners[event_type] << block
  end

  def emit(event_type, *args)
    return unless @listeners[event_type]

    @listeners[event_type].each do |listener|
      listener.call(*args)
    end
  end

  def process_with_hooks(data)
    emit(:before_process, data)

    result = if block_given?
      yield(data)
    else
      data.transform { |item| item * 2 }
    end

    emit(:after_process, result)
    result
  end
end

dispatcher = EventDispatcher.new

dispatcher.on(:before_process) { |data| puts "Processing #{data.size} items" }
dispatcher.on(:after_process) { |result| puts "Completed with #{result.size} results" }

result = dispatcher.process_with_hooks([1, 2, 3, 4]) do |numbers|
  numbers.map { |n| n ** 2 }
end
# Processing 4 items
# Completed with 4 results
# => [1, 4, 9, 16]

Common Pitfalls

Ruby developers encounter several recurring issues with yield and block_given? that stem from misunderstanding block scope, parameter handling, and execution context.

Forgetting to check block_given? before calling yield causes LocalJumpError when no block exists. This error occurs at runtime, not during method definition.

def broken_method(value)
  result = value * 2
  yield(result)  # Raises LocalJumpError if no block
end

def fixed_method(value)
  result = value * 2
  yield(result) if block_given?
  result
end

# broken_method(5)      # LocalJumpError: no block given (yield)
fixed_method(5)         # => 10
fixed_method(5) { |x| puts x }  # prints 10, returns 10

Block parameter mismatches create subtle bugs when yield passes different argument counts than the block expects. Ruby handles extra parameters by setting them to nil, and ignores missing parameters.

def parameter_confusion
  yield(1, 2, 3)
end

# Block expects fewer parameters - extras ignored
parameter_confusion { |a| puts a }  # prints 1

# Block expects more parameters - extras become nil
parameter_confusion { |a, b, c, d| puts "#{a}, #{b}, #{c}, #{d}" }
# prints "1, 2, 3, "

# Splat parameters capture extras
parameter_confusion { |first, *rest| puts "First: #{first}, Rest: #{rest}" }
# prints "First: 1, Rest: [2, 3]"

Variable scope issues arise when blocks close over variables that change between block creation and execution. The block captures variable references, not values.

def scope_trap
  multipliers = []

  3.times do |i|
    multipliers << lambda { |x| x * i }
  end

  # All lambdas reference the same 'i' variable
  multipliers.map { |m| m.call(10) }
end

scope_trap  # => [20, 20, 20] - all use final value of i (2)

def scope_fix
  multipliers = []

  3.times do |i|
    # Create new scope for each iteration
    multipliers << lambda { |x| x * i }
  end

  multipliers.map { |m| m.call(10) }
end

scope_fix  # => [0, 10, 20] - each lambda captures its own i value

Exception handling becomes complex when blocks raise errors that methods must manage. Unhandled block exceptions propagate through yield to the calling method.

def unsafe_processor(items)
  results = []
  items.each do |item|
    processed = yield(item)  # Block exceptions bubble up
    results << processed
  end
  results
end

def safe_processor(items)
  results = []
  failures = []

  items.each do |item|
    begin
      processed = yield(item)
      results << processed
    rescue StandardError => e
      failures << { item: item, error: e.message }
    end
  end

  { results: results, failures: failures }
end

data = [1, 2, "invalid", 4]

# unsafe_processor(data) { |x| x * 2 }  # Raises TypeError on "invalid"

result = safe_processor(data) { |x| x * 2 }
# => { results: [2, 4, 8], failures: [{ item: "invalid", error: "String can't be coerced into Integer" }] }

Block return values can cause unexpected behavior when methods expect specific types or values. The yield expression evaluates to whatever the block returns, including nil.

def return_value_confusion
  numbers = [1, 2, 3, 4, 5]

  # Expecting block to return boolean for filtering
  filtered = numbers.select do |num|
    yield(num)  # Block might not return boolean
  end

  filtered
end

# Block returns nil (puts returns nil)
result1 = return_value_confusion { |n| puts n if n.even? }
# prints 2, 4
# => [] - empty because puts returns nil (falsy)

# Block returns boolean
result2 = return_value_confusion { |n| n.even? }
# => [2, 4] - correct filtering

# Block returns truthy/falsy values
result3 = return_value_confusion { |n| n if n > 3 }
# => [4, 5] - numbers > 3 are truthy, nil is falsy

Multiple yield calls in a method can lead to performance issues when blocks perform expensive operations. Each yield fully executes the block code.

def inefficient_double_yield(data)
  puts "First pass:"
  yield(data)  # Expensive block runs once

  puts "Second pass:"
  yield(data)  # Same expensive block runs again
end

def efficient_cached_yield(data)
  puts "Processing once:"
  result = yield(data)  # Block runs once

  puts "First result: #{result}"
  puts "Second result: #{result}"  # Use cached result
end

# Inefficient - block code executes twice
inefficient_double_yield("test") do |input|
  puts "Expensive computation on #{input}"
  sleep(0.1)  # Simulate expensive work
  input.upcase
end

# Efficient - block code executes once
efficient_cached_yield("test") do |input|
  puts "Expensive computation on #{input}"
  sleep(0.1)  # Simulate expensive work
  input.upcase
end

Reference

Core Methods

Method Parameters Returns Description
yield *args Object Executes the block with given arguments
yield() None Object Executes the block with no arguments
block_given? None Boolean Returns true if a block was passed to the current method

Block Execution Patterns

Pattern Syntax Use Case
Simple yield yield Execute block without arguments
Parameterized yield yield(arg1, arg2) Pass data to block
Conditional yield yield if block_given? Optional block execution
Multiple yields 3.times { yield(i) } Repeated block execution
Capturing result result = yield(data) Process block return value

Block Parameter Handling

Block Declaration Yield Call Result
{ |a| ... } yield(1, 2, 3) a = 1, extras ignored
{ |a, b| ... } yield(1) a = 1, b = nil
{ |a, *rest| ... } yield(1, 2, 3) a = 1, rest = [2, 3]
{ |a, b=0| ... } yield(1) a = 1, b = 0

Exception Scenarios

Situation Exception Prevention
yield without block LocalJumpError Use block_given? check
Block raises exception Propagates through yield Wrap yield in begin/rescue
Invalid block parameters ArgumentError Match parameter expectations
Block returns unexpected type Logic errors Validate block return values

Performance Characteristics

Operation Time Complexity Notes
block_given? O(1) Constant time check
yield O(1) + block cost Block execution dominates
Multiple yields O(n) × block cost Linear with yield count
Block creation O(1) Closure capture cost varies

Common Method Patterns

# Iterator pattern
def each_item(collection)
  collection.each { |item| yield(item) }
end

# Builder pattern
def configure
  yield(self) if block_given?
  self
end

# Resource management
def with_resource
  resource = acquire_resource
  begin
    yield(resource)
  ensure
    release_resource(resource)
  end
end

# Callback registration
def on_event(&block)
  @callbacks ||= []
  @callbacks << block if block
end

def trigger_event(*args)
  @callbacks&.each { |callback| callback.call(*args) }
end

# Conditional processing
def process(data)
  if block_given?
    yield(data)
  else
    default_processing(data)
  end
end

Error Codes

Error Class Cause Solution
LocalJumpError yield called without block Add block_given? check
ArgumentError Block parameter count mismatch Match block signature to yield args
StandardError Block raises exception Handle in calling method
SystemStackError Infinite yield recursion Check termination conditions