Passing Blocks as Arguments (&block)
Almost every Ruby programmer starts by writing methods that accept a block with yield, like [1,2,3].each { |n| puts n }. But sometimes you need more than just running the block once — you need to store it, pass it along to another method, check whether one was given, or convert between blocks and full-fledged Proc objects. That’s what the &block syntax is for. Understanding it unlocks a huge amount of what makes Ruby’s method-oriented style feel so flexible.
Overview / How it works
Every method call in Ruby can optionally be given a block — the chunk of code between { } or do...end that follows the call. A block is not an object on its own; it’s a piece of syntax attached to a method invocation. Inside the method, you can run that block with the yield keyword, and you can check whether a block was supplied at all with block_given?.
That’s the simple case. But what if the block needs to be treated as data — stored in a variable, passed to a different method, or returned from the current one? For that, Ruby lets you capture the block by adding a special parameter prefixed with an ampersand, conventionally named &block, as the last parameter in a method definition. When you do this, Ruby converts the block into a real Proc object and binds it to that parameter name. Once it’s a Proc, it behaves like any other object: you can call .call on it, store it in an instance variable, pass it to other methods as a regular argument, or check .nil? if no block was given (an explicit &block parameter is nil, not an error, when the caller passes no block).
The ampersand is doing a conversion, and it works in both directions:
- In a method definition,
def foo(&block)converts the incoming block into aProcobject. - At a call site,
foo(&some_proc)converts aProc(or anything responding toto_proc, like aSymbol) back into a block for that call.
This second direction is exactly what powers the famous shorthand %w[a b c].map(&:upcase): Symbol#to_proc turns :upcase into a proc that calls .upcase on its argument, and the leading & turns that proc into a block for map.
Why does this matter? Because yield alone can only be used inside the exact method that received the block — it has no way to hand the block off to a helper method or store it for later. Capturing the block with &block gives you a first-class value you can move around your program like any other object.
Syntax
def method_name(regular_args, &block_param)
# block_param is now a Proc object, or nil if no block was given
block_param.call(some_value) # invoke it explicitly
yield(some_value) # or invoke the block implicitly, without capturing it
end
method_name(args) { |x| x * 2 } # pass a literal block
method_name(args, &a_proc) # convert an existing Proc into a block
| Piece | Meaning |
|---|---|
&block_param (in a def) |
Must be the last parameter; captures the block passed to this method as a Proc. |
&some_proc (in a call) |
Converts a Proc/Method/Symbol into a block for that specific call. |
yield |
Invokes the block attached to the current method call directly, without needing to capture it. |
block_given? |
Returns true/false depending on whether the current call received a block. |
Examples
Example 1: yield vs. an explicit &block parameter
def greet_yield
puts "Before"
puts yield("Ruby") if block_given?
puts "After"
end
def greet_explicit(&block)
puts "Before"
puts block.call("Ruby") if block
puts "After"
end
greet_yield { |name| "Hello, #{name}! (via yield)" }
greet_explicit { |name| "Hello, #{name}! (via &block)" }
Output:
Before
Hello, Ruby! (via yield)
After
Before
Hello, Ruby! (via &block)
After
Both methods do the same job. greet_yield never turns the block into an object — it just jumps into it with yield, which is slightly faster because no Proc is allocated. greet_explicit captures the block as block and calls block.call("Ruby") explicitly. The observable output is identical; the difference is only in what you can do with the block afterward.
Example 2: forwarding a block to another method
def with_logging(&block)
puts "Starting operation..."
result = run_task(&block)
puts "Finished with result: #{result}"
result
end
def run_task
yield(10, 20)
end
with_logging { |a, b| a + b }
Output:
Starting operation...
Finished with result: 30
This is the key use case for &block: with_logging doesn’t use the block itself — it needs to pass it along to run_task. It captures the block as a Proc named block, then re-converts it into a block with &block when calling run_task. Inside run_task, yield(10, 20) runs that forwarded block with arguments 10 and 20, producing 30, which flows back up through both return values.
Example 3: converting between blocks, Procs, and Symbol#to_proc
def apply_to_all(items, &operation)
items.map(&operation)
end
square = ->(n) { n * n }
puts apply_to_all([1, 2, 3], &square).inspect
puts apply_to_all([1, 2, 3]) { |n| n * n }.inspect
puts %w[hello world].map(&:upcase).inspect
Output:
[1, 4, 9]
[1, 4, 9]
["HELLO", "WORLD"]
The first call passes an existing lambda, square, preceded by & — this converts it into a block for apply_to_all, which captures it again as operation and forwards it into items.map(&operation). The second call passes a block literal instead, which apply_to_all captures the same way; the result is identical. The third line shows Symbol#to_proc: :upcase responds to to_proc by producing a proc equivalent to ->(s) { s.upcase }, and & turns that into a block for map.
How it works step by step
- When you call a method with a block literal (
{ }ordo...end), Ruby attaches that block to the call frame but does not allocate aProcobject unless something asks for one. - If the method body uses
yield, Ruby jumps straight into the attached block — no object is created, which is why plainyieldis the cheapest way to invoke a block. - If the method signature ends in
&name, Ruby wraps the attached block in a realProcobject and assigns it toname. If no block was given,nameis simplynil. block_given?checks a flag Ruby maintains per call frame — it works whether or not you’ve also captured the block with&name.- At a call site, prefixing an argument with
&does the reverse conversion: if the value is already aProc, it’s used directly as the block; otherwise, Ruby calls.to_procon it first (this is how&:upcaseand even&method(:puts)work) and uses the result as the block. - A method can only ever receive one block per call, which is why
&blockmust be the final parameter — it isn’t a regular argument competing for position with others.
Common Mistakes
Mistake 1: calling yield without checking block_given?
If a method calls yield but the caller doesn’t supply a block, Ruby raises a LocalJumpError at runtime. This mistake compiles fine — it only fails when the code actually runs.
def process
yield
end
process
Output:
(raises LocalJumpError: no block given (yield))
Always guard a yield call with block_given? (or give the method a sensible default behavior) unless the block is genuinely required and you want the error to surface loudly.
def process
if block_given?
yield
else
puts "No block given, skipping."
end
end
process
Output:
No block given, skipping.
Mistake 2: forgetting the & when forwarding a captured block
Once you’ve captured a block as a Proc with &block, passing that variable to another method without the leading & sends it as an ordinary positional argument — it does not attach as a block, so the receiving method’s own block_given? and yield see nothing.
def outer(&block)
inner(block)
end
def inner(x)
if block_given?
yield
else
puts "No block received! Got: #{x.class}"
end
end
outer { puts "Hi" }
Output:
No block received! Got: Proc
inner received block as a regular Proc argument named x, not as its own block — so block_given? inside inner is false. The fix is to re-prefix it with & when forwarding:
def outer(&block)
inner(&block)
end
def inner
if block_given?
yield
else
puts "No block received!"
end
end
outer { puts "Hi" }
Output:
Hi
Best Practices
- Prefer plain
yieldandblock_given?when a method only needs to run the block once itself — it’s simpler and avoids allocating aProcobject. - Only capture the block with
&blockwhen you actually need to treat it as an object: forwarding it to another method, storing it, or passing it to something likemaporeach_with_object. - Name the captured parameter
&block(or a more specific name like&callback) — it must be the last parameter in the method’s signature. - Remember an explicit
&blockparameter isnil, not an error, when no block is given — guard withif blockorblock&.callbefore invoking it. - Use
&:symbolfor the common one-method-call-per-element pattern instead of writing out{ |x| x.method_name }. - Keep in mind only
nilandfalseare falsy in Ruby, so a block that legitimately returns0,"", or[]should never be mistaken for “no block” — always test withblock_given?orblock.nil?, not the block’s return value.
Practice Exercises
- Write a method
repeat(n, &block)that calls the given blockntimes, passing the current iteration index (starting at 0) each time. Call it with a block that prints"Iteration: #{i}"forn = 3. - Write a method
timed(&block)that captures a block, prints"Running...", calls the block, prints"Done.", and returns the block’s result. Then write a second methodrun_twice(&block)that forwards its block intotimedtwice in a row and prints both results. - Given
words = ["ruby", "is", "fun"], use the&:symbolshorthand withmapto produce an array of the lengths of each word. Expected output:[4, 2, 3].
Summary
- A block is not an object by itself — it’s syntax attached to a method call, invoked with
yieldor checked withblock_given?. - Adding
&blockas a method’s last parameter converts the incoming block into a realProcobject you can store, inspect, or pass around. - Prefixing an argument with
&at a call site does the reverse: it converts aProc(or anything withto_proc, like aSymbol) into a block for that call. - To forward a captured block into another method, you must re-apply
&— passing the bare variable sends it as a normal argument instead. - Calling
yieldwhen no block was given raisesLocalJumpError; always guard withblock_given?unless the block is required. &:symbolis shorthand powered bySymbol#to_proc, and it’s the idiomatic way to write simple one-method-per-element transformations.
