yield and Block Arguments
In Ruby, methods can receive an implicit chunk of code called a block, and the yield keyword is how a method reaches back and runs that block. It’s the mechanism behind dozens of familiar methods like each, map, and times — each of those is just a plain method that calls yield internally. Understanding yield means understanding how Ruby lets you write methods that accept custom behavior from the caller without ever declaring a block as a formal parameter.
Overview: How yield Works
Every method in Ruby can optionally be called with a block attached — a chunk of code wrapped in do...end or curly braces that follows the method call. Unlike a regular argument, a block is not passed as part of the parameter list; it rides along invisibly, and the method decides whether and when to run it. The keyword yield is what actually runs it: when Ruby’s interpreter hits yield inside a method body, it pauses the method, executes the attached block, and then resumes the method with whatever value the block returned.
This is the mechanism behind nearly every Enumerable method you already use. [1, 2, 3].each { |n| puts n } is not special syntax — each is an ordinary method (implemented in C for the built-in classes, or in plain Ruby if you wrote it yourself) that loops over the array and calls yield(element) for each item. The block you write is simply what runs each time yield fires inside that method.
Because a block isn’t automatically an object, you can’t store it in a variable or pass it along to another method by name unless you capture it explicitly. Ruby gives you two related tools for that: block_given?, which lets a method check whether it was called with a block at all (calling yield with no block raises LocalJumpError), and an explicit &block parameter, which captures the block as a real Proc object bound to a local variable so you can call it with .call, store it, or forward it to another method.
yield is also an expression, not a statement — it evaluates to whatever the block’s last expression returns. That return value can be used just like any other value: assigned to a variable, passed to another method, or made the return value of the enclosing method itself (Ruby methods always return the value of their last-evaluated expression, with no explicit return required).
Syntax
def method_name(*args)
# ... some setup code ...
result = yield(optional_arguments)
# ... result is whatever the block returned ...
end
# Calling with a single-line block:
method_name(1, 2) { |param1, param2| param1 + param2 }
# Calling with a multi-line block:
method_name(1, 2) do |param1, param2|
param1 + param2
end
yield— calls the block that was passed to the current method, with no arguments.yield(arg1, arg2, ...)— calls the block, passing values that are bound to the block’s own parameters (declared between the pipes, e.g.|param1, param2|).block_given?— a boolean check, usually used to guard ayieldso the method doesn’t raise an error when called without a block.- An explicit
&blockparameter — turns the implicit block into a named, callableProcobject.
| Construct | Meaning |
|---|---|
yield |
Invoke the block passed to the current method, with no arguments |
yield(args) |
Invoke the block, passing one or more arguments to its parameters |
block_given? |
Returns true if the current method was called with a block, false otherwise |
&block |
Captures the passed block as an explicit Proc object bound to the parameter name |
Examples
Example 1: A basic yield
def greet
puts "Before yield"
yield
puts "After yield"
end
greet { puts "Hello from block!" }
Output:
Before yield
Hello from block!
After yield
greet prints its first line, then hits yield, which hands control to the block attached to the call — the block prints its own line — and then control returns to greet, which prints its final line. The block runs exactly where the yield appears, not before or after the method.
Example 2: Passing arguments with yield
def repeat(n)
n.times do |i|
yield i
end
end
repeat(3) { |i| puts "Iteration #{i}" }
Output:
Iteration 0
Iteration 1
Iteration 2
yield i passes a value to the block, which binds it to its own parameter i. Note that the yield here sits inside the block passed to n.times, yet it still calls the block passed to repeat — a detail explored further below.
Example 3: block_given? and yield’s return value
def calculate(a, b)
if block_given?
yield(a, b)
else
a + b
end
end
puts calculate(4, 5)
puts calculate(4, 5) { |x, y| x * y }
Output:
9
20
The first call has no block, so block_given? is false and calculate falls back to plain addition. The second call supplies a block, so yield(a, b) runs it, and the block’s return value (4 * 5) becomes the value of the if expression — and therefore the method’s own return value, since it’s the last expression evaluated.
Example 4: Capturing the block explicitly with &block
def process_items(items, &block)
items.each do |item|
block.call(item.upcase)
end
end
process_items(["ruby", "python", "go"]) { |name| puts "Language: #{name}" }
Output:
Language: RUBY
Language: PYTHON
Language: GO
Here the block is captured explicitly as &block, turning it into a real Proc object stored in the local variable block. That lets the method call it with .call from inside a nested each block, transform the argument first, and invoke it as many times as needed — something a bare yield can also do, but an explicit capture is useful when you need to pass the block along or store it for later.
How it works, step by step
Walking through repeat(3) { |i| puts "Iteration #{i}" } from Example 2:
- Ruby calls
repeat(3). The block{ |i| puts "Iteration #{i}" }is attached to this call but not evaluated yet — it’s simply remembered by the method invocation. - Inside
repeat,n.timesruns, which itself calls a block three times: once fori = 0, once fori = 1, once fori = 2. - On each pass, the line
yield iexecutes. Ruby suspends the current method and jumps to the block that was passed torepeat(not totimes), binding its parameterito the value passed. - The block body runs —
puts "Iteration #{i}"— and prints a line. - Control returns to right after the
yield icall inside thetimesblock, which then moves on to the next iteration. - Once
n.timeshas run all three iterations,repeathas no more code to execute and returns.
Notice the important detail in step 3: even though yield i is written inside the block passed to times, it still refers to the block passed to repeat. A yield always calls the block belonging to the nearest enclosing method definition, never a block belonging to another block.
Common Mistakes
Mistake 1: Calling yield without checking for a block
If a method calls yield but is invoked without a block, Ruby raises LocalJumpError. This is a common surprise for anyone used to languages where an optional callback simply does nothing if it’s missing.
def shout
yield.upcase
end
shout
# LocalJumpError: no block given (yield)
The fix is to guard the yield with block_given? so the method behaves sensibly either way:
def shout
return "no block given" unless block_given?
yield.upcase
end
puts shout
puts shout { "hello" }
Output:
no block given
HELLO
Mistake 2: Forgetting that yield’s return value is discarded unless it’s the last expression
A Ruby method always returns the value of its last evaluated expression, so a yield call in the middle of a method body throws away whatever the block returned — the method’s own trailing expression wins instead.
def build_message(name)
yield(name)
"Message built"
end
result = build_message("Ruby") { |n| "Hello, #{n}!" }
puts result
Output:
Message built
The block’s return value, "Hello, Ruby!", is computed and then silently discarded, because yield(name) isn’t the last line of the method. Removing the trailing string literal lets yield‘s result become the method’s own return value:
def build_message(name)
yield(name)
end
result = build_message("Ruby") { |n| "Hello, #{n}!" }
puts result
Output:
Hello, Ruby!
Best Practices
- Guard every
yieldwithblock_given?unless a block is genuinely required for the method to make sense — a clear fallback or a deliberateArgumentErrorbeats a crypticLocalJumpError. - Only capture the block explicitly with
&blockwhen you need to pass it along to another method, store it, or call it more than once — a bareyieldis more idiomatic and reads more clearly when you’re just invoking it inline. - Remember that
yieldpasses arguments to the block loosely: extra arguments are ignored and missing ones becomenil, the same lenient arity rules that apply toproc, not the strict rules oflambda. - Be deliberate about a method’s last expression whenever it also calls
yield— decide explicitly whether the method should return the block’s result or something else, rather than letting it happen by accident. - Prefer
yieldandblock_given?over older idioms like callingProc.newwith no arguments to grab an implicit block — it’s clearer and matches current Ruby style guides.
Practice Exercises
- Write a method
with_timingthat takes no arguments, prints"Starting...", callsyield, then prints"Done.". Call it with a block that prints"Working", and confirm the three lines print in the right order. - Write a method
safe_divide(a, b)that yields the result ofa / bto a block ifbis not zero, and yields the string"undefined"ifbis zero. Useblock_given?so calling it without a block returnsnilinstead of raising an error. Hint: you’ll need anif/elsearound theyieldcall. - Write a method
transform_all(array, &block)that builds and returns a new array by callingblock.callon every element ofarray. Test it withtransform_all([1, 2, 3]) { |n| n * n }and confirm the result is[1, 4, 9].
Summary
yieldinvokes the block attached to the current method call; it’s the mechanism behindeach,map, and every other block-taking method in Ruby.- A block is not an object by default — it can’t be stored or passed along unless captured explicitly with an
&blockparameter, which turns it into a realProc. block_given?lets a method check for a block before callingyield, avoidingLocalJumpErrorwhen no block was passed.yield(args)passes values to the block’s parameters leniently — extra args are dropped, missing ones becomenil.yieldis an expression: it evaluates to the block’s return value, which becomes the method’s own return value only if it’s the last thing the method evaluates.yieldalways calls the block belonging to the enclosing method definition, even when written inside a nested block.
