shift()

ES3+

Removes the first element from an array and returns that element.

Syntax

array.shift()

Return Value

any

The removed element

Examples

JavaScript
const fruits = ['apple', 'banana', 'cherry'];
const first = fruits.shift();
console.log(first);
console.log(fruits);
Output:
// 'apple' ['banana', 'cherry']

📌 When to Use

Use shift() when the oldest element must be taken first: draining a task queue in arrival order, processing buffered messages FIFO, consuming lines from a parsed file top to bottom, or trimming the oldest entries from a capped history after pushing new ones. Its semantics mirror pop() at the opposite end - remove, return, mutate - and the push()/shift() pairing is the minimal correct queue for small workloads. The honest caveat is cost: removing index 0 forces every remaining element down one slot, so each shift() is O(n). That is irrelevant for the queue sizes most applications see (event handlers, notification buffers, retry queues of dozens to a few thousand items) and disqualifying for high-throughput pipelines, where a head-index queue - keep the array intact, advance a pointer, occasionally slice off the consumed prefix - or a linked-list deque delivers true O(1) dequeues. Also weigh alternatives that avoid consuming at all: destructuring const [first, ...rest] = arr expresses "head and tail" immutably (also O(n), but non-mutating), and iterating with for...of processes in order without dismantling the array. Reserve shift() for cases where the array genuinely IS the mutable queue.

⚠️ Common Mistakes

Draining a large array with shift() in a loop. Each call re-indexes every remaining element, so consuming n items costs O(n^2) total - a 100,000-message backlog means roughly five billion element moves. The loop looks innocent and benchmarks fine at small sizes, then falls off a cliff in production. Iterate with an index or use a proper queue for large volumes.

Forgetting the mutation. shift() permanently removes the first element from the array itself, so "let me just grab the first item" inside a helper quietly shortens a list that other code still iterates - the classic symptom is a loop elsewhere that mysteriously processes one fewer item each time the helper runs.

Shifting when a read would do: arr[0] answers "what is first?" without destroying anything. Reaching for shift() out of habit turns an inspection into a consumption, and the missing element resurfaces as an off-by-one in whatever processes the array next.

Ignoring the empty case: shift() on an empty array returns undefined rather than throwing, so a worker loop written as const job = queue.shift(); job.run() crashes on job.run only after the queue drains - typically in the quiet moments that never occur during testing. Check queue.length first or guard the returned value.

Using while (queue.shift()) as the drain condition. A falsy queued value - 0, "", null - stops the loop with items still waiting. The length test while (queue.length) is immune, and it also reads as what it means: "while anything is queued".

✅ Best Practices

For throughput-sensitive queues, keep the array append-only and advance a head index: read arr[head++], and periodically compact with arr = arr.slice(head) once the consumed prefix grows large. Dequeue becomes O(1) with occasional amortized cleanup - the standard escape from shift()'s O(n).

When immutability is required, split the head off declaratively: const [first, ...rest] = arr binds both pieces in one statement, or pair arr[0] with arr.slice(1) when you prefer explicit method calls. Both leave the source intact for other consumers - the state-safe equivalent of shift().

Model small queues with push() in, shift() out - correct FIFO in two methods, ideal for event buffers and rate limiters holding dozens to a few thousand items. Wrap the pair in enqueue()/dequeue() functions so the implementation can later swap to a head-index queue without touching call sites.

Prefer for...of when the array does not need to end up empty: processing every element in order is iteration, not consumption. Destructive draining is only justified when the queue keeps receiving items between processing rounds.

Combine push() and shift() for fixed-size sliding windows: push the newest reading, and shift once length exceeds the window. With small windows (moving averages, sparkline buffers of 10-100 points), the O(n) shift is a handful of moves and the code stays transparent.

⚡ Performance Notes

shift() is O(n) per call: the spec requires every remaining element to move down one index, and although V8 optimizes the copy into fast memmove-style operations for packed arrays (and can sometimes adjust internally for small arrays), the linear cost is fundamental to the contiguous-array model. The compounding danger is loops: draining n elements by repeated shift() does n + (n-1) + ... + 1 moves - O(n^2) - which is why a message consumer that handled thousands of items per second in testing can stall completely on a million-item backlog. Thresholds to keep in mind: below roughly a thousand elements, shift() is effectively free and clarity should win; in the tens of thousands with frequent dequeues, switch to a head-index scheme (advance a pointer, slice off consumed items occasionally) or a linked-list/circular-buffer queue for true O(1). unshift() shares the same shifted-elements cost in reverse. Memory-wise shift() allocates nothing; all cost is element movement.

🌍 Real World Example

Processing a Message Queue

A message dispatcher must honor arrival order - the email queued first goes out first - and that fairness guarantee is exactly what the push()/shift() pairing encodes: producers append to the tail, the consumer takes from the head. The explicit length check before shifting gives the function a clean "queue empty" signal (returning null) instead of letting undefined leak into handleMessage(). Returning the processed message also makes the function testable and composable. At the scale of user-facing notification queues this implementation is ideal; if the same code someday fronts a firehose of events, the enqueue/dequeue call sites stay identical while the internals swap to a head-index queue.

const messageQueue = [];

function addMessage(message) {
  messageQueue.push(message);
}

function processNextMessage() {
  if (messageQueue.length === 0) {
    return null;
  }
  const message = messageQueue.shift();
  handleMessage(message);
  return message;
}

// Usage
addMessage({type: 'email', to: 'user@example.com'});
addMessage({type: 'sms', to: '+1234567890'});
processNextMessage();  // Processes email first (FIFO)

Related Methods