Passing Blocks as Arguments (&block)

Published by Programming Line in Ruby. Last updated .

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 a Proc object.
  • At a call site, foo(&some_proc) converts a Proc (or anything responding to to_proc, like a Symbol) 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 ({ } or do...end), Ruby attaches that block to the call frame but does not allocate a Proc object 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 plain yield is the cheapest way to invoke a block.
  • If the method signature ends in &name, Ruby wraps the attached block in a real Proc object and assigns it to name. If no block was given, name is simply nil.
  • 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 a Proc, it’s used directly as the block; otherwise, Ruby calls .to_proc on it first (this is how &:upcase and even &method(:puts) work) and uses the result as the block.
  • A method can only ever receive one block per call, which is why &block must 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 yield and block_given? when a method only needs to run the block once itself — it’s simpler and avoids allocating a Proc object.
  • Only capture the block with &block when you actually need to treat it as an object: forwarding it to another method, storing it, or passing it to something like map or each_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 &block parameter is nil, not an error, when no block is given — guard with if block or block&.call before invoking it.
  • Use &:symbol for the common one-method-call-per-element pattern instead of writing out { |x| x.method_name }.
  • Keep in mind only nil and false are falsy in Ruby, so a block that legitimately returns 0, "", or [] should never be mistaken for “no block” — always test with block_given? or block.nil?, not the block’s return value.

Practice Exercises

  • Write a method repeat(n, &block) that calls the given block n times, passing the current iteration index (starting at 0) each time. Call it with a block that prints "Iteration: #{i}" for n = 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 method run_twice(&block) that forwards its block into timed twice in a row and prints both results.
  • Given words = ["ruby", "is", "fun"], use the &:symbol shorthand with map to 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 yield or checked with block_given?.
  • Adding &block as a method’s last parameter converts the incoming block into a real Proc object you can store, inspect, or pass around.
  • Prefixing an argument with & at a call site does the reverse: it converts a Proc (or anything with to_proc, like a Symbol) 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 yield when no block was given raises LocalJumpError; always guard with block_given? unless the block is required.
  • &:symbol is shorthand powered by Symbol#to_proc, and it’s the idiomatic way to write simple one-method-per-element transformations.