Blocks Explained

Published by Programming Line in Ruby. Last updated .

A block in Ruby is a chunk of code you attach to a method call, written either with do...end or curly braces { }. Unlike a method or a Proc, a block is not itself an object — it has no independent existence outside the method call it’s passed to, and a method can accept at most one. Blocks are the mechanism behind everyday methods like each, map, and select, letting the caller supply custom behavior without the method’s author having to anticipate every use case in advance. Mastering blocks is the first step toward understanding Ruby’s whole block/proc/lambda family and toward writing code that reads the way experienced Rubyists expect it to.

Overview / How it works

When you write [1, 2, 3].each { |n| puts n }, the part in braces is a block. It is not evaluated where it’s written — instead, Ruby packages it up (along with the local variables visible at that point, which is what makes it a closure) and hands it to the each method implicitly. Inside each‘s own definition (which you don’t see, because it’s implemented in C, but conceptually looks like a loop), the block is invoked once per array element using the yield keyword. Each time yield runs, control jumps into your block with the given argument, the block body executes, and control returns to right after the yield call — carrying back whatever value the block’s last expression produced.

This is the key insight: a block is always attached to a specific method call. You cannot store a bare block in a variable, pass it around, or call it later on its own — for that you need to turn it into an object (a Proc), which is the subject of the next lesson. A block only exists for as long as the method it was passed to is running.

Because a block closes over its surrounding scope, it can read and modify local variables from the method that created it, even after that method has otherwise finished its own local work:

total = 0
[1, 2, 3].each { |n| total += n }
puts total

Here total is defined outside the block, but the block can see and mutate it because it was in scope when the block was written. This closure behavior is central to how blocks (and later, procs and lambdas) behave — they carry their birthplace’s variables with them.

Inside a method definition, you can check whether a caller supplied a block at all using block_given?, and you invoke the block with yield (optionally passing arguments, which become the block’s parameters). If you call yield in a method that received no block, Ruby raises a LocalJumpError — which is exactly why block_given? exists, to guard against that.

Syntax

collection.each do |item|
  # multi-line block body
end

collection.each { |item| item * 2 }
Form When to use it
do ... end Multi-line blocks; reads naturally when the block has several statements.
{ ... } Single-line blocks; conventional for short, expression-like bodies.
|param1, param2| Block parameters, declared right after the opening do or {.
yield Inside a method definition, invokes the block that was passed to that method call.
yield(arg1, arg2) Invokes the block, passing arguments that become its block parameters.
block_given? Returns true or false depending on whether the current method call received a block.

Note that do...end and { } have different precedence: { } binds tightly to the nearest method call, while do...end binds to the leftmost method call in the statement when parentheses are omitted. This distinction causes a classic mistake covered below.

Examples

Example 1: A basic block with each

[1, 2, 3].each do |n|
  puts n * 2
end

Output:

2
4
6

each calls the block once per element, binding n to 1, then 2, then 3 in turn. The block’s return value is discarded by each — it’s used purely for its side effect (puts), which is exactly what each is for, unlike map, which collects the block’s return values into a new array.

Example 2: Writing your own method that yields

def repeat(times)
  times.times do |i|
    yield i
  end
end

repeat(3) do |i|
  puts "Iteration #{i}"
end

Output:

Iteration 0
Iteration 1
Iteration 2

repeat takes no explicit block parameter in its signature, yet it can still call yield because Ruby always makes an implicit block available to a method if the caller supplied one. Each call to yield i hands control to the block attached to the repeat(3) do ... end call, passing the current index.

Example 3: Combining block_given? with map

def describe_numbers(numbers)
  if block_given?
    numbers.map { |n| yield n }
  else
    numbers
  end
end

result = describe_numbers([1, 2, 3]) { |n| n.even? ? "#{n} is even" : "#{n} is odd" }
puts result.join(", ")

Output:

1 is odd, 2 is even, 3 is odd

This is a realistic pattern: the method checks block_given? to decide its behavior, then uses yield inside another block (the one passed to map) to transform each element according to whatever the caller supplied. This shows blocks composing naturally — a block passed to describe_numbers gets invoked from within a block passed to map.

How it works step by step

Walking through Example 2 call by call:

  1. repeat(3) do |i| ... end is evaluated: Ruby calls repeat with the argument 3, and separately attaches the do...end block to that call.
  2. Inside repeat, times.times do |i| yield i end runs: 3.times loops three times, each time invoking its own block with i set to 0, then 1, then 2.
  3. That inner block’s body is yield i. Because yield always refers to the block passed to the enclosing method (repeat), not to the block passed to times, this jumps out to the block that was written at the call site: { |i| puts "Iteration #{i}" }.
  4. Control returns to times‘s loop after each yield completes, and the cycle repeats until 3.times is exhausted.
  5. repeat finishes and returns the value of times.times do ... end, which is the receiver 3 (unused here since we only cared about the side effects).

Common Mistakes

Mistake 1: Assuming a block always runs

A block does nothing on its own — the method it’s passed to must actually call yield for it to execute. If the method never yields, the block is silently ignored with no error at all:

def greet(name)
  puts "Hello, #{name}"
end

greet("Ruby") { puts "This line never runs" }

Output:

Hello, Ruby

Nothing signals that the block was ignored — greet simply never calls yield, so the block body never executes. If you intend for a method to accept and use a block, make sure it actually calls yield (and consider guarding with block_given? if the block is optional).

Mistake 2: Using return instead of next inside a block

Inside a block, return doesn’t just exit the block — it exits the entire enclosing method. This is easy to get wrong when you only meant to skip the current iteration:

def print_and_double(numbers)
  numbers.each do |n|
    return if n.negative?
    puts n * 2
  end
  puts "Done"
end

print_and_double([1, -2, 3])

Output:

2

The intent was probably “skip negative numbers,” but return exits print_and_double the moment it hits -2, so 3 is never processed and "Done" never prints. The fix is to use next, which only skips to the next iteration of the block:

def print_and_double(numbers)
  numbers.each do |n|
    next if n.negative?
    puts n * 2
  end
  puts "Done"
end

print_and_double([1, -2, 3])

Output:

2
6
Done

Mistake 3: Getting do...end vs { } precedence wrong

Because do...end binds to the leftmost method call rather than the nearest one, this looks like it maps and doubles, but it doesn’t:

puts [1, 2, 3].map do |x| x * 2 end

Here the do...end block attaches to puts, not to map — so map is called with no block at all (returning an Enumerator), and that Enumerator object is what gets printed, not the doubled numbers. The equivalent line written with braces, puts [1, 2, 3].map { |x| x * 2 }, binds the block to map as intended and prints 2, 4, 6 on separate lines. This is precisely why the style convention is to use { } for blocks whose return value feeds directly into another expression.

Best Practices

  • Use { } for short, single-line blocks and do...end for multi-line ones — this matches the community style guide and avoids the precedence trap above.
  • Guard with block_given? before calling yield in any method where the block is optional, rather than assuming the caller always supplies one.
  • Use next to skip an iteration and reserve return for genuinely wanting to exit the whole enclosing method early.
  • Give block parameters short but meaningful names (n, item, row) — single letters are fine for very short blocks, but name them for what they represent once the block does real work.
  • Prefer built-in Enumerable methods (map, select, reduce, each_with_index) over manually written loops with mutable counters — they express intent more clearly and read closer to the underlying business logic.
  • If you find yourself passing the same block to multiple calls, consider extracting it into a named method or a Proc instead of duplicating the block literal.

Practice Exercises

1. Write a method times_three(n) that yields 1 * 3, 2 * 3, … up to n * 3 to whatever block is passed to it. Calling it with a block that does puts should print the multiples of three from 3 up to n * 3.

2. Write a method safe_execute that returns the string "No block given" if called without a block, and otherwise calls the block and returns its result. Use block_given?.

3. Take the buggy print_and_double example from the Common Mistakes section and, without looking back at the fix, rewrite it so that negative numbers are skipped but positive numbers after them still get processed and "Done" still prints.

Summary

  • A block is an anonymous chunk of code attached to a method call using do...end or { } — it is not an object and can’t exist independently of that call.
  • Methods invoke a passed block with yield, and can check whether one was supplied at all with block_given?.
  • Blocks are closures: they can read and mutate local variables from the scope in which they were written.
  • return inside a block exits the enclosing method entirely; next just skips to the block’s next invocation — mixing these up is a very common bug.
  • do...end and { } have different precedence, which can silently change which method call a block attaches to.
  • Blocks are the foundation for Procs and Lambdas, covered next, which turn this same idea into a reusable, storable object.