Ruby Blocks and Iterators: Each, Map, Select, and Yield
Ruby blocks and iterators are what make the language feel natural and expressive. These two concepts sit at the heart of idiomatic Ruby: blocks let you pass chunks of behavior into methods, while iterators like each, map, and select use blocks to walk through collections. In this tutorial, you will learn what blocks are, how to use them with iterators, and how to create your own methods that accept blocks using yield.
intro context
Blocks are one of the first Ruby features that make code feel more like a conversation than a list of instructions. They let you hand a chunk of behaviour to a method at the exact point where that behaviour belongs. That is why they show up everywhere in Ruby, from collection traversal to DSL-style APIs in Rails and gems.
If you understand blocks early, a lot of Ruby code becomes easier to read. Methods such as each, map, and select stop looking mysterious, because they are mostly just methods that know how to yield to the code you gave them. Once that idea clicks, you can read both built-in and custom iterators with more confidence.
Key takeaways
- Blocks let you pass a chunk of code into a method.
- Iterators like
each,map,select, andreduceare the everyday tools that use blocks. yieldlets your own methods hand control to a block.block_given?is the safe way to see whether a caller provided a block.
What are blocks?
A block is a chunk of code you can pass to a method. It’s not an object itself, but it can be associated with a method call. Blocks are the foundation of Ruby’s iterator methods.
# A block attached to the `each` method
[1, 2, 3].each do |number|
puts "Number: #{number}"
end
Blocks can be written with do...end or with curly braces:
# Both are equivalent
[1, 2, 3].each { |number| puts "Number: #{number}" }
The do...end syntax is preferred for multi-line blocks, while curly braces work well for single-line blocks.
That style split is not just about taste. It makes longer code easier to scan, because the structure of the block matches the amount of work it does. When the block has several lines or nested logic, do...end gives the reader more room to breathe. When the block is tiny, braces keep the expression compact.
Using built-in iterators
Ruby provides many iterator methods on arrays, hashes, and other enumerables. These methods all follow the same pattern: they traverse a collection and yield each element to the block you provide. Once you understand each, the rest of the iterator family follows naturally.
The each method
The most fundamental iterator is each. It yields each element to your block, visiting every item in order. The block receives one element at a time and can do anything with it, from printing to accumulating results. This pattern is the foundation that map, select, and reduce all build on:
fruits = ["apple", "banana", "cherry"]
fruits.each do |fruit|
puts "I love #{fruit}"
end
# Output:
# I love apple
# I love banana
# I love cherry
For hashes, each yields key-value pairs:
person = { name: "Alice", age: 30, city: "London" }
person.each do |key, value|
puts "#{key}: #{value}"
end
With hashes, each yields two block parameters, one for the key and one for the value. This destructuring works automatically because Ruby assigns the two-element array that each yields to the two block parameters you declare.
The map method
With hashes, each yields two block parameters, one for the key and one for the value. This destructuring works automatically because Ruby assigns the two-element array that each yields to the two block parameters you declare.
map transforms each element and returns a new array. Unlike each, which returns the original collection, map collects the return value of every block invocation into a fresh array of the same length. Use map when you want a transformed copy and each when you only need the side effects:
numbers = [1, 2, 3, 4, 5]
squared = numbers.map { |n| n ** 2 }
puts squared # => [1, 4, 9, 16, 25]
This is one of Ruby’s most powerful methods for data transformation. The block receives each element and returns a new value; map assembles those returned values into a new array of the same length. Use map when you want a transformed copy and each when you only need the side effects.
The select and reject methods
Filter elements based on a condition. select keeps elements where the block returns a truthy value, while reject does the opposite, keeping elements where the block returns falsy:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = numbers.select { |n| n.even? }
puts evens # => [2, 4, 6, 8, 10]
odds = numbers.reject { |n| n.even? }
puts odds # => [1, 3, 5, 7, 9]
These two methods are mirror images. select and reject always produce subsets of the original, so the result array is never larger than the input. For more complex filtering where you need to transform and filter in one pass, filter_map (Ruby 2.7+) combines both operations.
The reduce method
reduce combines all elements into a single value. It walks the collection, carrying an accumulator forward through each iteration, and returns the final accumulated result:
numbers = [1, 2, 3, 4, 5]
sum = numbers.reduce(0) { |acc, n| acc + n }
puts sum # => 15
# Or using the shorthand :+ symbol
sum = numbers.reduce(:+)
puts sum # => 15
The shorthand :+ passes the symbol as a method name, and reduce calls it on each pair of accumulator and element. This symbol-to-proc shortcut works with any binary operator like :*, :-, or :/.
Creating methods with blocks using yield
You can create your own methods that accept blocks using the yield keyword. yield transfers control from the method to the block, runs the block’s code, and then returns control to the method:
def greet
puts "Before yield"
yield
puts "After yield"
end
greet { puts "Inside the block!" }
# Output:
# Before yield
# Inside the block!
# After yield
When you write your own block-taking methods, think carefully about where the block fits into the workflow. Usually the block should sit right in the middle of the method’s main job, not at the beginning or the end by accident. That placement makes it easier for the caller to understand what they control and what the method itself still owns.
Passing data to blocks
You can pass data to the block using arguments after yield. Whatever values you pass after the yield keyword become the block parameters inside the caller’s block:
def double_numbers(numbers)
numbers.map { |n| yield n }
end
result = double_numbers([1, 2, 3, 4]) { |n| n * 2 }
puts result # => [2, 4, 6, 8]
Checking if a block was given
Use block_given? to check whether a block was provided:
def maybe_increment(number)
if block_given?
yield(number)
else
number + 1
end
end
puts maybe_increment(5) # => 6
puts maybe_increment(5) { |n| n * 3 } # => 15
block_given? is the safety net for optional blocks. Without it, calling yield when no block was provided raises a LocalJumpError. Always check with block_given? when the block is truly optional.
Common iterator patterns
Chaining methods
Ruby iterators can be chained for powerful data processing. Each method returns a new enumerable, so you can pipe the output of one operation directly into the next:
result = (1..20)
.select(&:odd?)
.map { |n| n ** 2 }
.first(5)
puts result # => [1, 9, 25, 49, 81]
The point of chaining is not to be clever, it is to let each step do one obvious job. A selection step narrows the data, a map step transforms it, and a final step picks the value you want to keep. That separation is one reason Ruby collections are so readable in practice.
Iterating with index
The point of chaining is not to be clever; it is to let each step do one obvious job. A selection step narrows the data, a map step transforms it, and a final step picks the value you want to keep. That separation is one reason Ruby collections are so readable in practice — each line does one thing well.
Use each_with_index when you need the position of each element. It adds a second block parameter for the zero-based index:
fruits = ["apple", "banana", "cherry"]
fruits.each_with_index do |fruit, index|
puts "#{index + 1}. #{fruit}"
end
# Output:
# 1. apple
# 2. banana
# 3. cherry
The index starts at zero, so adding one gives you a human-friendly count. For more control, with_index (without each_) can be chained after any enumerator and accepts a starting offset.
Range iterators
Ruby ranges support iteration directly. Both inclusive (..) and exclusive (...) ranges can be treated as collections and walked with blocks:
# Inclusive range
(1..5).each { |n| print "#{n} " }
# Output: 1 2 3 4 5
# Exclusive range
(1...5).each { |n| print "#{n} " }
# Output: 1 2 3 4
Blocks also help you describe a transformation in a way that reads like a pipeline. Instead of writing a manual loop, you can hand each step to the collection method that already knows how to walk through the values. That keeps the code short, but more importantly it keeps the intent visible.
Ranges are especially useful when you need a predictable sequence for testing, demos, or a simple loop with no collection already in hand. They keep the example small and make it obvious how many steps are being visited. An inclusive range (..) includes the end value, while an exclusive range (...) stops one short, which is convenient for zero-based counting.
When to use blocks
Blocks are perfect for:
- Callbacks: Run code after or around a method’s main action
- Data transformation: Map arrays to new values
- Filtering: Select or reject elements based on conditions
- Accumulation: Reduce collections to single values
- Resource management: Ensure cleanup happens (like file handling)
Common mistakes
- Using
eachwhen the code really needs a transformed result andmapwould be clearer. - Forgetting that
yieldwill raise an error if no block was given. - Writing very long blocks where a named method would explain the intent better.
- Chaining too many iterator calls in a way that hides the important step.
Frequently asked questions
Are blocks the same as Procs?
Not exactly. A block is attached to a method call, while a Proc is an object you can store and pass around. They are related, but the block syntax is the everyday Ruby tool most people use first.
When should I use yield?
Use yield when your method should hand control back to a caller-provided block at a specific point. That is common for wrappers, hooks, and helper methods that need to run custom logic in the middle of their own work.
Should I always prefer iterators over loops?
No. Iterators are often clearer, but a simple loop can still be the most readable choice when the task is straightforward. The goal is not to avoid loops at all costs, but to choose the clearest tool for the shape of the work.
Summary
Blocks and iterators are what make Ruby feel like Ruby. You now understand:
- What blocks are and how to write them
- Common built-in iterators:
each,map,select,reject,reduce - How to create your own methods with
yield - How to check for blocks with
block_given? - How to chain iterator methods for expressive data processing
forward-link
Once blocks feel natural, you can use them everywhere else in Ruby: with hashes, with file iteration, and with custom methods that expose their own small APIs. The next tutorial in the Ruby basics path is Ruby methods fundamentals, which shows how blocks fit into ordinary method design.
The more you use blocks, the more you will notice that Ruby code starts to read like a description of the work instead of a long list of steps. That style is one of Ruby’s biggest strengths, and iterators are a big part of how it stays readable.
Practice using these methods with your own data. The more you use them, the more natural they’ll feel.
See also
- Ruby methods fundamentals — how methods, blocks, and parameters work together
- Working with Ruby hashes — iterating over key-value pairs with blocks
- Ruby arrays — collection iteration and transformation