Ruby Array Methods: The Practical Guide

· 7 min read · Updated March 31, 2026 · beginner
ruby arrays enumerable methods

Ruby arrays come with a powerful set of built-in methods. Most real-world code reaches for the same handful over and over. This guide covers those: the methods that turn messy data into clean results.

map

map transforms every element in an array and returns a new array with the results. The original array stays untouched.

numbers = [1, 2, 3]
doubled = numbers.map { |n| n * 2 }
# => [2, 4, 6]

doubled
# => [2, 4, 6]

numbers
# => [1, 2, 3]

Return value: A new array with each element replaced by the block’s return value.

Gotcha: map always returns an array, even if the block returns nil or mixes types:

[1, 2, 3].map { |n| n.odd? ? n : nil }
# => [1, nil, 3]

If you chain map off an each or forget to use map, you lose the transformation:

# WRONG — map is silently ignored:
[1, 2, 3].each { |n| n * 2 }
# => [1, 2, 3]  (original, no transformation happened)

select and reject

select keeps elements where the block returns truthy. reject does the opposite — it keeps elements where the block returns falsy.

numbers = [1, 2, 3, 4, 5]

numbers.select(&:odd?)
# => [1, 3, 5]

numbers.reject(&:odd?)
# => [2, 4]

Return value: A new array. Neither method mutates the original.

Common gotcha: Both always return an array — even when empty:

[1, 2, 3].select { |n| n > 99 }
# => []  (empty array, not nil)

This means select is safe to chain without worrying about nil. The same applies to reject.

select and reject are also available as filter and filter_reject if that reads more naturally in your code.

reduce

reduce accumulates all elements into a single value. You provide an initial accumulator that gets passed through each iteration.

[1, 2, 3, 4].reduce(0) { |acc, n| acc + n }
# => 10

# With no initial value, the first element becomes the accumulator:
[1, 2, 3, 4].reduce { |acc, n| acc + n }
# => 10

Return value: A single value of whatever type your accumulator starts as.

Common gotcha: Forgetting the initial value when your block expects a specific type:

# WRONG — starts with string "0" but tries to call + on an integer:
["a", "b", "c"].reduce { |acc, n| acc + n }
# => TypeError: String can't be coerced into Integer

# RIGHT — start with empty string:
["a", "b", "c"].reduce("") { |acc, n| acc + n }
# => "abc"

reduce is also available as inject — both do exactly the same thing.

find

find returns the first element that matches your condition. If nothing matches, it returns nil.

users = [
  { name: "Alice", role: "dev" },
  { name: "Bob", role: "admin" },
  { name: "Carol", role: "dev" }
]

users.find { |u| u[:role] == "admin" }
# => {:name=>"Bob", :role=>"admin"}

users.find { |u| u[:role] == "superuser" }
# => nil

Return value: The matching element, or nil if nothing matches.

Gotcha: Because find can return nil, chaining off it without guards causes NoMethodError:

# DANGEROUS — crashes if no admin exists:
users.find { |u| u[:role] == "admin" }[:email]
# => NoMethodError: undefined method `[]' for nil:NilClass

# SAFE — use &. for optional chaining:
users.find { |u| u[:role] == "admin" }&.[](:email)
# => nil (if no admin)

The safe navigation operator &. is your friend with find.

sort_by

sort_by sorts elements based on a value you extract from each one. It’s cleaner and faster than using sort with a manual comparison block.

words = ["banana", "apple", "cherry", "date"]

words.sort_by(&:length)
# => ["date", "apple", "banana", "cherry"]

words.sort_by { |w| w.chars.first }
# => ["apple", "banana", "cherry", "date"]  (alphabetical by first letter)

Return value: A new sorted array. The original is unchanged.

Gotcha: Always use sort_by over sort when comparing computed values. With sort, your comparison block runs O(n²) times; with sort_by, the key extraction runs O(n) times:

# Slow — calls downcase twice per comparison:
words.sort { |a, b| a.downcase <=> b.downcase }

# Fast — calls downcase once per element:
words.sort_by(&:downcase)

uniq

uniq removes duplicate elements, preserving the order of first appearances.

[1, 2, 2, 3, 1, 4, 3].uniq
# => [1, 2, 3, 4]

Return value: A new array without duplicates.

Gotcha — the bang variant is tricky:

a = [1, 2, 3]
b = a.uniq!
# => nil  (no changes needed, already unique)

b
# => nil  # NOT the array — nil!

a
# => [1, 2, 3]  # original unchanged too

uniq! returns nil when no duplicates were removed. This is different from most bang methods. Never write arr.uniq! || arr — it always returns arr even when uniq! actually worked.

flatten

flatten collapses nested arrays into a single flat array.

nested = [[1, 2], [3, [4, 5]], 6]
nested.flatten
# => [1, 2, 3, 4, 5, 6]

# With a depth argument:
nested.flatten(1)
# => [1, 2, 3, [4, 5], 6]

Return value: A new flat array.

Gotcha: flatten only flattens arrays — it doesn’t recursively flatten hashes or other nested structures:

[[1, 2], { a: 3 }].flatten
# => [1, 2, {:a=>3}]  # hash stays as-is

For deeply nested or irregular structures, consider writing a recursive custom method instead.


Chaining Methods

One of Ruby’s strengths is that most array methods return arrays, which means you can chain them together cleanly.

# Get squared values of even numbers from 1 to 10
(1..10).to_a
  .select(&:even?)
  .map { |n| n ** 2 }
# => [4, 16, 36, 64, 100]
# Find the first long word and upcase it
words = ["cat", "elephant", "dog", "rhinoceros"]
words
  .select { |w| w.length > 4 }
  .find { |w| w.start_with?("r") }
  &.upcase
# => "RHINOCEROS"
# Sort users by age, extract their names as symbols
users = [
  { name: "Carol", age: 34 },
  { name: "Alice", age: 28 },
  { name: "Bob", age: 41 }
]
users
  .sort_by { |u| u[:age] }
  .map { |u| u[:name].to_sym }
# => [:Alice, :Carol, :Bob]

The key insight: select, reject, map, uniq, flatten all return arrays and accept arrays — so they chain in any order. find breaks the chain because it returns a single element or nil, so always put it at the end or guard it with &..


Mutating vs Non-Mutating

Most array methods return a new array. The bang (!) variants mutate the original. Here’s the breakdown for the methods in this guide:

MethodNon-mutatingMutating
sort_bysort_by { }sort_by! { }
uniquniquniq!
flattenflattenflatten!
selectselect { }
rejectreject { }
mapmap { }
reducereduce
findfind { }

select, reject, map, reduce, and find have no mutating bang variants. If you need to mutate, assign the result back: numbers = numbers.map { |n| n * 2 }.

Note: uniq! is an exception — it returns nil when no changes are made, unlike other bang methods that return the mutated array.


Common Patterns

Filter, Transform, Deduplicate

Extract usernames from admin users, downcase them, remove duplicates:

users = [
  { name: "Alice", roles: ["admin", "dev"] },
  { name: "Bob", roles: ["dev"] },
  { name: "CAROL", roles: ["admin", "dev"] }
]

users
  .select { |u| u[:roles].include?("admin") }
  .map { |u| u[:name].downcase }
  .uniq
# => ["alice", "carol"]

Build a Frequency Map with Reduce

Count how many times each word appears:

words = %w[apple banana apple cherry banana apple]

frequency = words.reduce(Hash.new(0)) do |counts, word|
  counts[word] += 1
  counts
end
# => {"apple"=>3, "banana"=>2, "cherry"=>1}

Hash.new(0) creates a hash where missing keys default to 0, making the counting logic clean.

Safe Nested Extraction

Find a user and safely dig into nested attributes:

users = [
  { id: 1, metadata: { email: "alice@example.com" } },
  { id: 2, metadata: {} }
]

# Find user 1 and get their email, without crashing on nil metadata
users
  .find { |u| u[:id] == 1 }
  &.dig(:metadata, :email)
# => "alice@example.com"

users
  .find { |u| u[:id] == 99 }
  &.dig(:metadata, :email)
# => nil  (no crash, no user 99)

&. (safe navigation) combined with dig handles missing keys gracefully.


See Also