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:
| Method | Where it adds | Mutates? | Returns |
|---|---|---|---|
unshift(*objs) | front (index 0) | yes | self |
insert(0, *objs) | at any index | yes | self |
push(*objs) | back | yes | self |
[x, *arr] | front | no | new list |
[1, 2] + [3, 4] | end | no | new 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
- Forgetting the splat.
arr.unshift(other_arr)insertsother_arras a single nested element. Usearr.unshift(*other_arr). - FrozenError on frozen receivers. Even a zero-arg call triggers the modification check.
- O(n²) loops. Repeated
unshiftis slow on long sequences. Batch the arguments. - Naming collision with
Module#prepend. TheArray#prependalias shares a name with the module mixin feature. Context tells you which. - Return value confusion.
unshiftreturns 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)