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 |