Enumerable#each_with_index
enum.each_with_index { |item, index| block } -> enum The each_with_index method iterates over each element in a collection while providing both the element and its index position. This is useful when you need to know where an element is located during iteration.
Basic Usage
fruits = ["apple", "banana", "cherry"]
fruits.each_with_index { |fruit, index| puts "#{index}: #{fruit}" }
# 0: apple
# 1: banana
# 2: cherry
The zero-based index that appears in the block is the default behavior and matches how arrays work internally. When you need to capture each pair for later use, converting the enumerator to an array wraps the element and its index together as a two-element sub-array.
Converting to Array
numbers = [10, 20, 30]
result = numbers.each_with_index.to_a
# => [[10, 0], [20, 1], [30, 2]]
This conversion is useful when you want to inspect the pairs directly or hand them to another part of the program. Because the index travels with each item, the resulting array keeps the relationship between value and position intact. If the index should become a lookup key, you can redirect the pairs into a hash with each_with_object, where the accumulator starts empty and each element lands under its index.
Building a hash from index
items = ["a", "b", "c"]
indexed = items.each_with_index.each_with_object({}) { |(item, idx), hash|
hash[idx] = item.upcase
}
# => { 0 => "A", 1 => "B", 2 => "C" }
Turning the enumerator into a hash is a nice way to keep the positions available under numeric keys. That pattern can make lookups easier when the index itself matters more than the original array shape. To display a numbered list that starts at one instead of zero, chain with_index to shift the offset while keeping the underlying enumeration in order.
With an offset start
# Start index at 1 instead of 0
list = ["first", "second", "third"]
list.each_with_index.with_index(1) { |item, idx| puts "#{idx}. #{item}" }
# 1. first
# 2. second
# 3. third
Starting from one is sometimes friendlier when the index is meant for display. The important part is that the underlying enumeration still knows it is walking the collection in order, even if the printed number is adjusted for people.
Practical Examples
Numbered lists
tasks = ["Buy milk", "Walk dog", "Write code"]
tasks.each_with_index { |task, i| puts "#{i + 1}. [ ] #{task}" }
# 1. [ ] Buy milk
# 2. [ ] Walk dog
# 3. [ ] Write code
Numbered lists are the classic use case because the index and the text are both part of the output. Using each_with_index keeps the loop compact and avoids a separate counter variable that has to be maintained by hand. The same pattern extends to parallel arrays, where the index aligns values from two separate collections without requiring a zip operation.
Processing paired data
names = ["Alice", "Bob", "Carol"]
ages = [25, 30, 35]
names.each_with_index { |name, i| puts "#{name} is #{ages[i]} years old" }
# Alice is 25 years old
# Bob is 30 years old
# Carol is 35 years old
Paired data works well here because the same index can line up values from multiple arrays. That makes the method a handy choice when two collections are parallel and the current position is the thing that ties them together. You can also use the index to filter or locate specific positions, for example picking items at even indices or finding the first element at a particular offset.
Finding by index
items = ["a", "b", "c", "d", "e"]
# Find first item at even index
items.each_with_index.find { |_, idx| idx.even? }
# => "a"
# Get all items at odd indices
items.each_with_index.select { |_, idx| idx.odd? }.map(&:first)
# => ["b", "d"]
Searching by index is another good match because the block can look at both the content and the position at the same time. That makes it easy to ask questions like “which items are in even positions?” without writing manual loop counters.
Transforming with index
words = ["hello", "world", "ruby"]
numbered = words.each_with_index.map { |word, idx| "#{idx + 1}. #{word}" }
# => ["1. hello", "2. world", "3. ruby"]
Transformation stays simple because the index is already available in the block. You can use it to build labels, apply offsets, or shape the result in ways that would otherwise need extra bookkeeping. The method also works with hashes, where the index tracks insertion order and the block receives each key-value pair as a two-element array.
With hashes
stock = { apples: 5, bananas: 3, oranges: 2 }
stock.each_with_index { |(key, value), idx| puts "#{idx}: #{key} = #{value}" }
# 0: apples = 5
# 1: bananas = 3
# 2: oranges = 2
Hashes do not have a natural numeric position in the same way arrays do, but the index can still be useful when you want a stable display order. This is especially handy in logging or reporting, where the pair order matters more than the collection type itself.
Chaining with other enumerators
[1, 2, 3, 4, 5].each_with_index
.select { |n, i| i.even? }
.map(&:first)
# => [1, 3, 5]
Chaining keeps the method flexible because each_with_index can hand off to other enumerator methods without forcing you to collect the whole result first. That makes it fit nicely into pipelines that do a little filtering and then a final transformation.
Return Value
Returns an Enumerator if no block is given. When a block is provided, returns the original collection.
[1, 2, 3].each_with_index { }
# => [1, 2, 3]
The return behavior is easy to forget, but it is what lets the method participate in longer chains. If a block is given, you still get the original collection back, which means the method can slot into a larger expression without losing the source array.
Performance Note
each_with_index is slightly slower than each due to the overhead of tracking the index. For simple iteration where index isn’t needed, use each instead.
That cost is usually tiny, so the practical question is readability rather than raw speed. If the index helps explain the loop, the method often pays for itself by making the code easier to understand.
Practical Notes
This method is a good fit any time the index is part of the meaning of the loop. Numbered output, paired arrays, and position-based transformations all read more cleanly when the index arrives with the element instead of being tracked by hand. The method also helps avoid separate counter variables, which can reduce off-by-one mistakes.
Because the index is zero-based, it is often worth adjusting the display value in the block when people are the audience. Adding one at the point of formatting keeps the code honest about the underlying array positions while still producing a friendlier result.
See Also
Array#each_slice— iterate over consecutive elementsEnumerable#each_with_object— iteration that carries a memo objectEnumerable#find— find elements by conditionArray#map— transform elements