Ruby blocks aren't just functions
2025-07-07When you call a method in Ruby, you can pass a block in addition to the method’s arguments,
delimited with do/end or curly braces.
This is an additional piece of code which trails the method call.
The method can execute the block any number of times using the yield keyword, optionally passing some arguments.
def run_twice
yield 'first time'
yield 'second time'
end
# vvvvv Block argument
run_twice do |str|
puts "Block called: #{str}"
end
# => Block called: first time
# => Block called: second time
On the surface, blocks appear equivalent to passing a lambda, closure, or anonymous function as a regular argument. However, blocks have additional capabilities not found in other languages.
Let’s explore the benefits of Ruby’s design, and how it enables custom control-flow which feels like a real part of the language, in a way that anonymous functions can’t achieve.
Blocks aren’t arguments
In the prior example, run_twice didn’t define any arguments:
def run_twice
Blocks are passed outside the usual argument list, and every method implicitly accepts a block, even if it never uses it. 1
Passing something callable to a method isn’t a concept unique to Ruby. To pick one language as an example, it’s also common in JavaScript to pass anonymous functions around:
function runTwice(fn) {
fn();
fn();
}
runTwice(str => {
console.log(`Function called: ${str}`);
});
In JavaScript, the anonymous function here is just another argument, in the same way you’d pass an integer or a string. It would be exactly equivalent to store the anonymous function in a variable first:
const fn = str => {
console.log(`Function called: ${str}`);
};
runTwice(fn);
Ruby doesn’t have an identical equivalent for this. Each block is bound to a specific method call. 2
Blocks for iteration
Ruby uses blocks to implement common looping and iteration patterns:
# Count-based loop using `Integer#times`
10.times do |i|
puts "Iteration #{i}"
end
# => Iteration 0
# => Iteration 1
# => ...
# For-each loop using `Array#each`
["foo", "bar", "baz"].each do |item|
puts item
end
# => foo
# => bar
# => baz
Methods like times and each call the block for each iteration, passing the current item as an argument.
Even though many languages technically support implementing iteration like this, Ruby is rare in how heavily it leans in. 3
When you’re iterating in any language, you expect to be able to break early.
Sure enough, Ruby has a break keyword for this:
["foo", "bar", "baz"].each do |item|
puts item
break if item.start_with?("b")
end
# => foo
# => bar
# (no baz)
But iteration isn’t a primitive, it’s using blocks - how does this work?
break aborts the call to the method which the block was passed into - in this example, the each method call.
This showcases what’s special about blocks when compared to anonymous functions: blocks can influence control-flow of their wider scope, not just within the block itself.
This is why it’s necessary for blocks to be attached to a specific function call.
It needs to be obvious to the language which method you want to break out of.
Any equivalent is very clunky to achieve in a language which uses anonymous functions. The closest you can get is throwing and catching a specialised exception:
const breakException = new Error();
try {
["foo", "bar", "baz"].forEach(item => {
console.log(item);
if (item.startsWith("b"))
throw breakException;
});
} catch (e) {
// Make sure we re-throw if it was a *real* exception
if (e !== breakException)
throw e;
}
This alone makes Ruby’s blocks feel much more like a proper control-flow construct than anonymous functions.
In JavaScript, only the loop constructs baked into the language (like for) can support break.
Blocks are nested inside methods
Blocks aren’t the target of a return statement.
A return in Ruby will always return from the enclosing method.
This means you can return from a method early, from within a block:
def find_needle_in_haystack(needle, haystack)
haystack.each do |candidate|
if candidate.contains?(needle)
# Actually returns from `find_needle_in_haystack`
# (Not the block)
return candidate
end
end
raise 'Needle not found'
end
In languages with anonymous functions, return typically targets the anonymous function instead, which isn’t always the behaviour you’d like.
This makes anonymous functions more difficult to integrate seamlessly as control-flow constructs.
You can still abort from an individual block invocation early, using the separate next keyword:
5.times do |i|
# Print only the odd numbers
next if i.even?
puts i
end
# => 1
# => 3
Blocks with values
Blocks aren’t limited to just procedural operations - they can produce values too.
When a method calls yield, it receives the final value the block, just like calling an anonymous function and getting its return value.
This means Ruby’s functional programming methods all take blocks which are used to process values:
[1, 2, 3, 4, 5]
.map { |x| x ** 2 }
.reject { |x| x < 10 }
# => [16, 25]
You can still use keywords like break for these applications, although there’s less need for that in functional-style code.
What about generators?
If you’re familiar with JavaScript or Python’s generators, the yield keyword might look a bit familiar!
Generators have similar semantics to a block if you use a for loop to exhaust all the values, and you can still break and continue like in Ruby:
function* twice() {
yield "first time";
yield "second time";
}
for (const str of twice()) {
console.log(`Loop called: ${str}`);
break;
}
// => Loop called: first time
// (No second time)
However, you can’t access the value returned by the for loop’s body from your generator.
In other words, yield doesn’t return any value - the for loop can only contain purely procedural code.
Blocks as lifetimes
Blocks are useful for more than just iteration. They can also help you encapsulate resources with some special lifetime.
Ruby doesn’t need a system like Python’s context managers, because it can use blocks instead. For example, you can open a file for the duration of a block, and it’ll get closed automatically at the end:
File.open("some_file.txt", "r") do |file|
# Do something with the file ^^^^
end
# File automatically closed here
Opinion 180
When I first learned Ruby, I was a little put off by the block system. I find it harder to grok language concepts that aren’t “first-class” like objects. Anonymous functions made more sense to me, as they’re just another kind of object, one that you can call.
After working with the language for many years, I can see the value and versatility of the design decisions behind blocks.
I tried to implement my own language with function-based control-flow, Babble, and encountered many of the issues which Ruby’s blocks managed to fix.
Sometimes, when a design decision doesn’t match a pattern, it’s for a good reason.
-
This is occasionally a gotcha because of Ruby’s lax whitespace and parenthesis rules - you can pass a block to the wrong method, and it silently doesn’t execute. ↩
-
You can technically capture a block into a
Procobject, using a special argument prefixed with&. I’m not going to cover this in much detail, since if you do that and try to pass theProcaround, you’ve pretty much just got an anonymous function. If you start passing this around, the tricks withreturnandbreakno longer work - Ruby will raise an exception if you try to use them. ↩ -
Ruby does technically have a syntactic
forloop construct, but it’s barely ever used in real code, and is actually just sugar for the#eachmethod with a block! ↩