rubyguides

Enumerable#reject

What does reject do?

reject returns an array containing all elements for which the block evaluates to falsy. It filters a collection by removing elements that match a condition — keeping everything that does not match.

Unlike find, which returns only the first non-matching element, reject collects all non-matching elements into a new array. If all elements match, it returns an empty array [].

reject is the logical opposite of select — where select keeps matches, reject discards them.

Basic Usage

With a block

[1, 2, 3, 4, 5].reject { |n| n > 2 }   # => [1, 2]
[1, 2, 3, 4, 5].reject { |n| n > 10 }  # => [1, 2, 3, 4, 5]

Both calls return a new array where elements that satisfy the condition have been removed. The first call drops 3, 4, and 5 because they are greater than 2, leaving only [1, 2]. The second call keeps everything because no element exceeds 10 — reject never raises an error when nothing matches; it simply returns a full copy of the original array. This mirrors select behaviour in reverse.

Without a block

When called without a block or argument, reject returns an Enumerator:

[1, 2, 3].reject  # => #<Enumerator: [1, 2, 3]:reject>

Calling reject without a block returns an Enumerator — a lazy object that wraps the collection and the pending operation. No filtering happens until you actually consume values from the enumerator. This deferred execution is what enables chaining with methods like with_index; the enumerator holds the intent and waits for a consumer to trigger the actual iteration. The enumerator knows the collection and the method it will eventually call, but it defers all work until a consumer demands the first value.

This allows chaining with other methods:

[1, 2, 3, 4, 5].reject.with_index { |n, i| i > 2 }
# => [1, 2, 3]

The with_index modifier attaches a zero-based index to each iteration, so the block now receives both the element and its position. Elements at indices 0, 1, and 2 (1, 2, 3) are kept because the condition i > 2 is false for those positions. Elements at indices 3 and 4 (4 and 5) are rejected because their indices exceed 2. This pattern is useful when filtering by position rather than by value.

Filtering Arrays

By numeric condition

temperatures = [12, 15, 8, 22, 19, -3, 25]
cold_days = temperatures.reject { |t| t > 10 }
cold_days  # => [8, -3]

The block keeps temperatures of 10 or below by rejecting anything warmer. Because reject treats true as “remove this element,” the condition reads directly as “reject temperatures greater than 10.” The original array is untouched — cold_days is a new array, so you can still access the full temperatures list for other calculations after this filter runs.

Using symbolic method references

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

words = ["cat", "dog", "elephant", "bee"]
short_words = words.reject { |w| w.length > 3 }
short_words  # => ["cat", "dog", "bee"]

The &:odd? shorthand rejects odd numbers, leaving only evens — a more direct phrasing than select(&:even?) when the exclusion is the natural way to think about the rule. The second example removes any word longer than three characters, keeping only "cat", "dog", and "bee". Both patterns produce new arrays and rely on the block returning a boolean that reject interprets as “should I drop this?”

Chaining with other Enumerable methods

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  .reject { |n| n.even? }
  .map { |n| n * 2 }
# => [2, 6, 10, 14, 18]

The chain first removes even numbers (keeping only odds), then doubles each survivor. The order is significant: if map ran first, every number would be doubled, and reject(&:even?) would then remove all of them because doubled integers are always even. This illustrates a general rule — filtering before transformation usually reduces the amount of work the transformation step must perform.

Filtering Hashes

When called on a hash, reject passes each key-value pair to the block:

ages = { alice: 30, bob: 17, charlie: 25, diana: 16 }

minors = ages.reject { |name, age| age >= 18 }
minors  # => { bob: 17, diana: 16 }

# Removing entries where the key matches a pattern
config = { host: "localhost", port: 5432, ssl: false, debug: true }
non_debug = config.reject { |k, v| k.to_s.include?("ssl") || k == :debug }
non_debug  # => { host: "localhost", port: 5432 }

When called on a hash, reject yields both the key and the value to the block. The first example keeps entries where age is below 18 by rejecting adults. The second example strips configuration keys whose name suggests debug or SSL settings. Both return a new hash rather than a new array, preserving the collection type — this is one way reject differs from map when applied to hashes.

Using hash value as filter criterion

products = {
  widget: { price: 100, in_stock: true },
  gadget: { price: 200, in_stock: false },
  doodad: { price: 50, in_stock: true }
}

out_of_stock = products.reject { |_, info| info[:in_stock] }
out_of_stock.keys  # => [:gadget]

cheap = products.reject { |_, info| info[:price] > 75 }
cheap.keys  # => [:doodad]

Nested hash filtering uses an underscore for the outer key when only the value matters. The first call rejects in-stock products, leaving only the out-of-stock gadget. The second rejects anything priced above 75, keeping the doodad at 50. Both examples show how reject naturally expresses exclusion rules: “reject what is in stock” and “reject what is expensive.”

reject vs select

reject and select are complementary — they partition a collection based on the inverse condition:

MethodReturns elements where block is…Equivalent to…
selecttruthy (matching)keeping matches
rejectfalsy (non-matching)removing matches
numbers = [1, 2, 3, 4, 5]

numbers.select { |n| n > 3 }    # => [4, 5] - keep > 3
numbers.reject { |n| n > 3 }   # => [1, 2, 3] - remove > 3

The condition n > 3 appears in both calls, but the outcome flips: select keeps the elements that make it true, and reject removes those same elements. When you read reject aloud as “reject elements where,” the behaviour maps directly to natural language, which is one reason the pair is so commonly used together as complementary filter operations.

Together, they partition a collection:

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

passing = numbers.select    { |n| n >= 3 }
failing = numbers.reject    { |n| n >= 3 }

passing  # => [3, 4, 5]
failing  # => [1, 2]

Running select and reject side by side with the same condition produces two complementary arrays that together contain every original element exactly once. Both methods preserve the original order, so [3, 4, 5] and [1, 2] maintain the relative sequence from the input array. For cases where you genuinely need both groups, Ruby provides a convenience method that does the split in a single pass rather than two.

This is equivalent to using partition:

passing, failing = numbers.partition { |n| n >= 3 }
passing  # => [3, 4, 5]
failing  # => [1, 2]

partition does what select and reject together accomplish, but in a single pass instead of two. For large collections, this can cut the iteration cost in half. The tradeoff is that partition always returns both groups, even when you only need one — but it is cleaner than writing two separate method calls when you genuinely want both halves of the split.

When to use reject instead of select

Prefer reject when:

  • You want to express what to remove rather than what to keep
  • The removal condition is simpler or more natural to express
  • You’re filtering out error cases or invalid entries
# select reads well when keeping matches
valid_users = users.select { |u| u.active? && u.verified? }

# reject reads well when removing invalid entries
invalid_users = users.reject { |u| u.active? && u.verified? }

The choice between select and reject is often about which phrasing matches the mental model. If you think “keep the active, verified users,” select aligns with that thought. If you think “discard anyone who is not active and verified,” reject expresses that intention more directly. Both produce the same partition; the difference is purely in readability and how the surrounding code frames the problem.

Performance Considerations

reject always iterates through the entire collection — it cannot short-circuit like find or any?. The time complexity is O(n) regardless of when non-matches are found.

# Always checks all elements, even if first one does not match
[1, 2, 3, 4, 5].reject { |n| n > 1 }
# Checks: 1, 2, 3, 4, 5 (all five iterations)

Like select, reject cannot short-circuit — it must inspect every element to guarantee it collects all non-matching items. Even if the first element already satisfies the rejection condition, the remaining elements are still checked. If early termination is what you need, find or a manual loop with break may serve better. The full-scan guarantee is a tradeoff: it ensures correctness at the cost of touching every element, even when the result is already determined.

Memory usage

reject creates a new array containing all non-matching elements. For very large collections with many matches, consider lazy iteration:

# Eager: builds full array first
result = (1..1_000_000).reject { |n| n.odd? }

# Lazy: yields matches one at a time
result = (1..1_000_000).lazy.reject { |n| n.odd? }
# then take what you need: result.first(10)

The eager version allocates an array of 500,000 even numbers before you call first(10), wasting memory and CPU on elements you never use. The lazy version computes only the first ten evens and stops, making it practical for large or unbounded enumerations. The tradeoff is that lazy enumerators add a small per-element overhead, so for small collections the eager form is usually faster overall.

Practical Examples

Filtering out invalid records

users = [
  { name: "Alice", role: "admin", active: true },
  { name: "Bob", role: "editor", active: false },
  { name: "Carol", role: "admin", active: true },
  { name: "Dave", role: "viewer", active: false }
]

inactive = users.reject { |u| u[:active] }
inactive.map { |u| u[:name] }  # => ["Bob", "Dave"]

This filters out active users, leaving only those with active: false. The result is then mapped to extract just the names. Writing reject { |u| u[:active] } reads as “reject users where active is truthy,” which is cleaner than the equivalent select { |u| !u[:active] } because it avoids the double negative.

Excluding certain file types

require "pathname"

files = Pathname.new(".")
ruby_files = files.children.reject { |p| p.file? && p.extname != ".rb" }
ruby_files  # => [#<Pathname:app.rb>, #<Pathname:config.rb>, ...]

This pattern filters a directory listing to keep only Ruby files by rejecting anything that is a file but does not have a .rb extension. The double condition p.file? && p.extname != ".rb" ensures that directories and other non-file entries pass through without being checked for an extension. The result is an array of Pathname objects that you can pass directly to other file-processing methods.

Removing nil or falsy values

values = [1, nil, 2, false, 3, nil, 4]
clean = values.reject(&:nil?)
clean  # => [1, 2, false, 3, 4]

# To also remove falsy values:
clean = values.reject { |v| !v }
clean  # => [1, 2, 3, 4]

The first call uses reject(&:nil?) to drop only nil values, leaving false intact as a legitimate data value. The second call rejects any value where !v is true — which catches both nil and false — producing a truly clean array of only truthy entries. Choose the stricter version when false is not meaningful in your data context.

Chaining filter operations

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# First remove odds, then keep only numbers greater than 4
result = numbers
  .reject(&:odd?)
  .reject { |n| n <= 4 }

result  # => [6, 8, 10]

# Equivalent using select:
result = numbers
  .select(&:even?)
  .select { |n| n > 4 }

result  # => [6, 8, 10]

Two consecutive reject calls are equivalent to two consecutive select calls with inverted conditions, as the second example shows. The chained reject version first drops odds (keeping evens), then drops numbers at or below 4. Using select instead simply rephrases the criteria: “keep evens” and “keep numbers above 4.” Both pipelines produce the same final array.

With sum or count

transactions = [
  { description: "Salary", amount: 3000 },
  { description: "Rent", amount: -1000 },
  { description: "Groceries", amount: -150 },
  { description: "Bonus", amount: 500 }
]

expenses = transactions.reject { |t| t[:amount] > 0 }
expense_total = expenses.sum { |t| t[:amount].abs }

expense_total  # => 1150

This example uses reject to isolate expense transactions (negative amounts) from income transactions (positive amounts), then calls sum on the filtered list. The abs call on each amount converts negative values to positive so the sum reflects actual money spent. This pattern is a one-liner alternative to a manual loop with a running total and conditional accumulation.

Cleaning up configuration

env = {
  RAILS_ENV: "production",
  DEBUG: "false",
  DATABASE_URL: "postgres://localhost/myapp",
  LOG_LEVEL: "info"
}

# Remove empty string and "false" config values
clean_env = env.reject { |_, v| v == "" || v == "false" }
clean_env  # => { RAILS_ENV: "production", DATABASE_URL: "postgres://localhost/myapp", LOG_LEVEL: "info" }

Cleaning configuration hashes with reject is a natural fit because the mental model is “remove the entries I do not want.” The block checks each value for empty strings or the literal "false" string and drops matching entries. The result is a hash containing only the keys with meaningful, active values — a common pattern in environment-configuration sanitisation.

Common mistakes

The most common mistake is using reject when the condition is really easier to read as a positive match. If the rule naturally says “keep the active users,” select may be clearer. If the rule says “drop the inactive users,” reject is usually the better fit.

Another mistake is forgetting that reject returns a new collection. If you need to mutate the original array or hash, use the bang version of another method or assign the result back explicitly.

Summary

reject is a clean way to remove matching entries from a collection while keeping the rest. It reads naturally when you think in terms of what to exclude, and it pairs well with select and partition when you are working through filtering logic.

See Also