JS Array Shift and Unshift Methods: Full Tutorial

Learn how the JavaScript shift() and unshift() methods work for adding and removing elements at the start of arrays. Covers syntax, return values, performance costs, queue implementation, and when to prefer alternatives.

JavaScriptbeginner
11 min read

While push() and pop() operate on the end of an array, shift() and unshift() do the same at the beginning. These methods are essential for queue-based patterns, priority insertion, and any workflow where new data arrives at the front. However, they carry a significant performance cost that every developer should understand before using them in production.

The shift() Method

shift() removes the first element from an array and returns it. All remaining elements shift down by one index:

javascriptjavascript
const notifications = ["Welcome!", "New message", "Friend request"];
 
const first = notifications.shift();
console.log(first);         // "Welcome!"
console.log(notifications); // ["New message", "Friend request"]

Syntax

javascriptjavascript
const removedElement = array.shift()

How shift() Re-Indexes Elements

When shift() removes the first element, every remaining element must move one position forward:

javascriptjavascript
const before = ["a", "b", "c", "d"];
// Indices:     0     1     2     3
 
before.shift(); // removes "a"
 
const after = before;
// Now:   ["b", "c", "d"]
// Indices: 0     1     2   โ† every index changed

This re-indexing is the reason shift() runs in O(n) time. For an array with 1 million elements, the engine must update 999,999 index positions.

Shifting from an Empty Array

Like pop(), calling shift() on an empty array returns undefined:

javascriptjavascript
const empty = [];
console.log(empty.shift()); // undefined
console.log(empty.length);  // 0

The unshift() Method

unshift() adds one or more elements to the beginning of an array and returns the new length:

javascriptjavascript
const queue = ["Bob", "Carol"];
 
const newLength = queue.unshift("Alice");
console.log(queue);     // ["Alice", "Bob", "Carol"]
console.log(newLength); // 3

Adding Multiple Elements

Pass multiple arguments to insert them all at the front, preserving their argument order:

javascriptjavascript
const numbers = [4, 5, 6];
 
numbers.unshift(1, 2, 3);
console.log(numbers); // [1, 2, 3, 4, 5, 6]
Insertion Order

unshift(1, 2, 3) inserts all three as a group. The result is [1, 2, 3, ...], not [3, 2, 1, ...]. If you call unshift() three separate times with one argument each, the order reverses: unshift(1), unshift(2), unshift(3) produces [3, 2, 1, ...].

Return Value

Like push(), unshift() returns the new length, not the array:

javascriptjavascript
const arr = ["x"];
const result = arr.unshift("a", "b");
console.log(result); // 3 (the new length)
console.log(arr);    // ["a", "b", "x"]

shift() and unshift() at a Glance

Propertyshift()unshift()
ActionRemoves first elementAdds element(s) to start
ParametersNoneOne or more values
ReturnsRemoved elementNew array length
Mutates originalYesYes
Time complexityO(n)O(n)
On empty arrayReturns undefinedAdds normally

Building a Queue with shift() and push()

A queue follows First-In-First-Out (FIFO) order. New items enter at the back (push) and leave from the front (shift):

javascriptjavascript
class PrintQueue {
  constructor() {
    this.jobs = [];
  }
 
  addJob(document) {
    this.jobs.push({
      name: document,
      addedAt: new Date().toISOString(),
    });
    console.log(`Queued: ${document} (${this.jobs.length} in queue)`);
  }
 
  processNext() {
    if (this.jobs.length === 0) {
      console.log("Queue is empty");
      return null;
    }
    const job = this.jobs.shift();
    console.log(`Printing: ${job.name}`);
    return job;
  }
 
  peek() {
    return this.jobs[0] || null;
  }
 
  get size() {
    return this.jobs.length;
  }
}
 
const printer = new PrintQueue();
printer.addJob("Invoice_2026.pdf");   // Queued: Invoice_2026.pdf (1 in queue)
printer.addJob("Report_Q1.docx");     // Queued: Report_Q1.docx (2 in queue)
printer.addJob("Contract_Draft.pdf"); // Queued: Contract_Draft.pdf (3 in queue)
 
printer.processNext(); // Printing: Invoice_2026.pdf
printer.processNext(); // Printing: Report_Q1.docx
console.log(printer.size); // 1

Real-World Patterns

Recent Activity Feed

Adding new items to the top of a feed while keeping a maximum size:

javascriptjavascript
class ActivityFeed {
  constructor(maxItems = 50) {
    this.items = [];
    this.maxItems = maxItems;
  }
 
  addActivity(activity) {
    this.items.unshift({
      ...activity,
      timestamp: Date.now(),
    });
 
    // Trim oldest entries if over limit
    if (this.items.length > this.maxItems) {
      this.items.length = this.maxItems;
    }
  }
 
  getRecent(count = 10) {
    return this.items.slice(0, count);
  }
}
 
const feed = new ActivityFeed(100);
feed.addActivity({ user: "Alice", action: "created a post" });
feed.addActivity({ user: "Bob", action: "liked a photo" });
feed.addActivity({ user: "Carol", action: "commented on a thread" });
 
// Most recent first
console.log(feed.getRecent(2));
// [{ user: "Carol", ... }, { user: "Bob", ... }]

Priority Task Processing

Inserting high-priority items at the front of a task list:

javascriptjavascript
const taskQueue = [];
 
function addTask(task, priority = "normal") {
  const entry = { task, priority, addedAt: Date.now() };
 
  if (priority === "urgent") {
    taskQueue.unshift(entry); // Goes to front
  } else {
    taskQueue.push(entry);   // Goes to back
  }
}
 
function processNextTask() {
  return taskQueue.shift(); // Always process from front
}
 
addTask("Send weekly report", "normal");
addTask("Update documentation", "normal");
addTask("Fix production crash", "urgent");  // Jumps to front
addTask("Review pull request", "normal");
 
console.log(processNextTask().task); // "Fix production crash"
console.log(processNextTask().task); // "Send weekly report"

Sliding Window Buffer

Using push() and shift() together to maintain a fixed-size buffer:

javascriptjavascript
class SlidingWindow {
  constructor(windowSize) {
    this.buffer = [];
    this.windowSize = windowSize;
  }
 
  add(value) {
    this.buffer.push(value);
    if (this.buffer.length > this.windowSize) {
      this.buffer.shift(); // Remove oldest when window is full
    }
  }
 
  average() {
    if (this.buffer.length === 0) return 0;
    const sum = this.buffer.reduce((acc, val) => acc + val, 0);
    return sum / this.buffer.length;
  }
}
 
// Moving average of the last 5 temperature readings
const tempWindow = new SlidingWindow(5);
[72, 68, 75, 71, 69, 80, 82].forEach(temp => {
  tempWindow.add(temp);
  console.log(`Reading: ${temp}, Moving avg: ${tempWindow.average().toFixed(1)}`);
});
// Reading: 72, Moving avg: 72.0
// Reading: 68, Moving avg: 70.0
// ...
// Reading: 82, Moving avg: 75.4

Performance: Why Start Operations Are Slow

The fundamental difference between end operations and start operations comes down to re-indexing:

OperationWhat happens internallyTime
push(x)Append at index length, increment lengthO(1)
pop()Remove at index length-1, decrement lengthO(1)
unshift(x)Shift every element right by 1, insert at index 0O(n)
shift()Remove index 0, shift every element left by 1O(n)
javascriptjavascript
// Demonstrate the performance gap
function benchmark(label, fn) {
  const start = performance.now();
  fn();
  const elapsed = performance.now() - start;
  console.log(`${label}: ${elapsed.toFixed(2)}ms`);
}
 
const size = 100_000;
 
benchmark("push 100K", () => {
  const arr = [];
  for (let i = 0; i < size; i++) arr.push(i);
});
// push 100K: ~5ms
 
benchmark("unshift 100K", () => {
  const arr = [];
  for (let i = 0; i < size; i++) arr.unshift(i);
});
// unshift 100K: ~2000ms+ (400x slower)
Avoid unshift() in Loops

Calling unshift() inside a loop with a large array creates quadratic O(n^2) time complexity. Each call shifts all existing elements. If you need to build an array in reverse order, use push() and then call reverse() once at the end, or use Array.from() with a mapping function.

Faster Alternatives to shift()/unshift()

When performance matters, restructure your logic to avoid start operations:

javascriptjavascript
// Instead of unshift() in a loop:
// Slow: O(n^2)
const slow = [];
for (let i = 0; i < 10000; i++) {
  slow.unshift(i);
}
 
// Fast: O(n) โ€” push then reverse
const fast = [];
for (let i = 0; i < 10000; i++) {
  fast.push(i);
}
fast.reverse();
 
// The result is identical, but "fast" is orders of magnitude quicker

For queue patterns on large datasets, consider a linked list or a circular buffer instead of an array with shift().

Comparison: All Four Mutation Methods

MethodPositionReturnsTimeUse case
push()EndNew lengthO(1)Stack push, appending
pop()EndRemoved elementO(1)Stack pop, removing last
unshift()StartNew lengthO(n)Priority insertion
shift()StartRemoved elementO(n)Queue dequeue, FIFO

Immutable Alternatives

When you need to avoid mutating the original array (React state, Redux, pure functions):

javascriptjavascript
const original = ["b", "c", "d"];
 
// Instead of unshift("a"):
const withFront = ["a", ...original];
console.log(withFront); // ["a", "b", "c", "d"]
console.log(original);  // ["b", "c", "d"] โ€” unchanged
 
// Instead of shift():
const [removed, ...rest] = original;
console.log(removed); // "b"
console.log(rest);    // ["c", "d"]
console.log(original); // ["b", "c", "d"] โ€” unchanged

Common Mistakes

Confusing the return value of unshift():

javascriptjavascript
// Bug: unshift returns the new length, not the array
const arr = [2, 3];
const result = arr.unshift(1);
console.log(result); // 3 (length, not the array)

Using shift() to clear an array:

javascriptjavascript
// Slow: O(n^2) total โ€” avoid this pattern
const items = [1, 2, 3, 4, 5];
while (items.length > 0) {
  items.shift(); // Re-indexes on every call
}
 
// Fast: O(1)
items.length = 0;

Not realizing unshift preserves argument order:

javascriptjavascript
const arr = [4, 5];
 
// This inserts [1, 2, 3] as a group at the front
arr.unshift(1, 2, 3);
console.log(arr); // [1, 2, 3, 4, 5] โœ“
 
// Calling separately reverses the order
const arr2 = [4, 5];
arr2.unshift(3);
arr2.unshift(2);
arr2.unshift(1);
console.log(arr2); // [1, 2, 3, 4, 5] โ€” same result only because we called in sequence
 
// But this would differ:
const arr3 = [4, 5];
arr3.unshift(1);
arr3.unshift(2);
arr3.unshift(3);
console.log(arr3); // [3, 2, 1, 4, 5] โ€” reversed!
Rune AI

Rune AI

Key Insights

  • shift() removes from start, unshift() adds to start: Both are O(n) because they re-index every element
  • Queue pattern: Combine push() (enqueue at back) and shift() (dequeue from front) for FIFO order
  • Return values: shift() returns the removed element; unshift() returns the new length
  • Performance awareness: Avoid shift()/unshift() in loops over large arrays; use push() + reverse() instead
  • Immutable alternatives: Use [newItem, ...arr] and const [first, ...rest] = arr when mutation is not acceptable
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between shift() and pop()?

Both remove a single element and return it. `shift()` removes from the beginning (index 0) and `pop()` removes from the end (last index). The critical difference is performance: `pop()` runs in O(1) time while `shift()` runs in O(n) because it must re-index every remaining element.

When should I use unshift() instead of push()?

Use `unshift()` when the order of elements matters and new items must appear at the start of the array, such as activity feeds, priority queues, or breadcrumb navigation. If order does not matter or you can sort afterward, prefer `push()` for its O(1) performance.

Does shift() modify the original array?

Yes. `shift()` is a mutating method. It removes the first element from the original array and shifts all remaining elements down by one index. If you need the original array preserved, use destructuring (`const [first, ...rest] = arr`) or `slice(1)` instead.

How do I unshift without mutating the original array?

Use the spread operator: `const result = [newElement, ...originalArray]`. This creates a new array with the new element at the front and all original elements after it. The original array remains unchanged.

Why is shift() so slow for large arrays?

When you remove index 0, every other element must be moved: index 1 becomes 0, index 2 becomes 1, and so on. For an array with N elements, this requires N-1 move operations. This is inherent to how JavaScript arrays store data in contiguous indexed positions. For high-performance queue needs, use a linked list or a ring buffer.

Conclusion

shift() and unshift() complete the quartet of basic array mutation methods alongside push() and pop(). They enable queue patterns, priority insertion, and front-of-array manipulation, but their O(n) time complexity means they should be used deliberately. For small arrays or low-frequency operations they work well. For large arrays in tight loops, restructure your code to use push()/pop() with a final reverse(), or switch to a data structure designed for efficient front operations.