rubyguides

Array#unshift

unshift(*objects) → self

Signature

unshift(*objects) → self

Array#unshift prepends one or more elements to the front of the receiver. The method takes a splat, so you can pass any number of arguments. It mutates the receiver in place and returns the same object (not a copy, not the prepended elements).

Basic Usage

Prepending a single value:

arr = [2, 3, 4]
arr.unshift(1)        # => [1, 2, 3, 4]
arr                   # => [1, 2, 3, 4]

Pass more than one value and the elements are inserted in order, with the leftmost argument landing at index 0. The rest follow at indices 1, 2, and so on — there is no reversing or shuffling. What you pass is what shows up at the front, in the order you passed it:

arr = [3, 4]
arr.unshift(1, 2)      # => [1, 2, 3, 4]

This is equivalent to arr = [1, 2, *arr], except the original object is mutated rather than rebound.

The prepend Alias

Ruby 2.5 added Array#prepend as an alias for Array#unshift. It exists for symmetry with Array#append, the alias of Array#push:

arr = [3, 4]
arr.prepend(1, 2)      # => [1, 2, 3, 4]

The two methods are the same C function under the hood. Pick whichever reads better in context. Watch out: Module#prepend is a totally unrelated concept (it inserts a module into the ancestor chain). Same word, different feature.

Splatting another array

To prepend the contents of another list, splat it. Forgetting the splat nests it as a single element — a frequent source of “why is there a list inside my list?” bugs:

arr = [3, 4]
head = [1, 2]
arr.unshift(*head)     # => [1, 2, 3, 4]   # flattened
arr.unshift(head)      # => [[1, 2], 1, 2, 3, 4]   # nested

The splat expands head into positional arguments, so each element is inserted individually.

Zero-Argument Call

unshift accepts any number of arguments, including zero. Calling it with nothing to add leaves the receiver untouched — no error, no warning, just a no-op that returns self. That said, the implementation still runs its modification check on the receiver, which matters the moment you freeze it:

arr = [1, 2, 3]
arr.unshift            # => [1, 2, 3]
arr.unshift()          # => [1, 2, 3]

Frozen Receivers

Calling unshift on a frozen receiver raises FrozenError, even with zero arguments. The zero-arg path still goes through the modification check, so freezing turns every call into an error — not just the ones that would actually change contents:

arr = [1, 2, 3].freeze
arr.unshift(0)
# => FrozenError (can't modify frozen Array)

If mutation isn’t an option — frozen receivers, shared state, concurrent access — you can build a new list from the front. A literal that splats the original gives you the same shape unshift would have produced, and the source stays put:

frozen = [2, 3].freeze
new_arr = [1, *frozen]   # => [1, 2, 3]   # frozen is unchanged

Return Value Is self

unshift returns the receiver — the same object, mutated. This trips up people coming from languages where the method would return a fresh collection:

arr = [1, 2, 3]
result = arr.unshift(0)
result.equal?(arr)     # => true   # same object, not a copy

The official docs use this pattern: a.unshift(:bam, :bat) # => [:bam, :bat, :foo, "bar", 2]. The => shows the new contents, but the return value is the receiver, with those contents.

unshift vs insert vs +

unshift is a special case of Array#insert at index 0. Both mutate and return self. The + operator and bracket-literal syntax do not mutate:

MethodWhere it addsMutates?Returns
unshift(*objs)front (index 0)yesself
insert(0, *objs)at any indexyesself
push(*objs)backyesself
[x, *arr]frontnonew list
[1, 2] + [3, 4]endnonew list
arr = [1, 2, 3]
arr.unshift(0)         # => [0, 1, 2, 3]    # same as insert(0, 0)
arr.insert(1, :a)      # => [0, :a, 1, 2, 3] # mid-array, unshift can't do this

If you don’t want mutation, prefer the non-mutating form. A literal like [x, *arr] or [1, 2] + arr builds a fresh list, so there’s no chance of accidentally mutating something you didn’t mean to. It also skips the in-place memmove that unshift performs, which is a noticeable speedup on long sequences.

Performance

Each call to unshift shifts every existing element right by the number of arguments prepended. That’s an O(n) memmove per call. If you call unshift once, that’s fine. Calling it in a loop is O(n²):

# Slow — O(n²) over the array
items = (1..10_000).to_a
items.unshift(:marker)
items.unshift(:other_marker)

# Faster — batch the prepends
items = (1..10_000).to_a
items.unshift(:marker, :other_marker)   # one O(n) memmove

The batching advice applies any time you’re building a list at the front. As an alternative, push to the end and reverse once when you’re done — a single O(n) walk that performs no per-element memmove at all.

Common Gotchas

  1. Forgetting the splat. arr.unshift(other_arr) inserts other_arr as a single nested element. Use arr.unshift(*other_arr).
  2. FrozenError on frozen receivers. Even a zero-arg call triggers the modification check.
  3. O(n²) loops. Repeated unshift is slow on long sequences. Batch the arguments.
  4. Naming collision with Module#prepend. The Array#prepend alias shares a name with the module mixin feature. Context tells you which.
  5. Return value confusion. unshift returns the receiver, not a fresh collection. Don’t chain it with another mutator expecting a new list.

See Also

  • Array#prepend — the alias added in Ruby 2.5
  • Array#shift — removes an element from the front (the opposite operation)
  • Array#push — appends to the back (the symmetric append)