JS Array Push and Pop Methods: A Complete Guide

Master the JavaScript push() and pop() methods for adding and removing elements from the end of arrays. Covers syntax, return values, real-world patterns, performance characteristics, stack implementation, and common pitfalls.

JavaScriptbeginner
11 min read

push() and pop() are the two most-used array methods in JavaScript. They add and remove elements from the end of an array, making them the foundation of stack-based patterns, queue processing, and everyday list manipulation. Despite their simplicity, developers regularly misuse their return values, accidentally mutate shared state, and miss performance implications when working with large datasets.

This guide covers both methods in depth with real-world examples and patterns you will actually use in production code.

The push() Method

push() adds one or more elements to the end of an array and returns the new length (not the array itself):

javascriptjavascript
const cart = ["Laptop", "Mouse"];
 
const newLength = cart.push("Keyboard");
console.log(cart);      // ["Laptop", "Mouse", "Keyboard"]
console.log(newLength); // 3

Syntax

javascriptjavascript
array.push(element1)
array.push(element1, element2, ..., elementN)

Pushing Multiple Elements

You can add several elements in a single call:

javascriptjavascript
const colors = ["red"];
 
colors.push("green", "blue", "yellow");
console.log(colors); // ["red", "green", "blue", "yellow"]

Pushing from Another Array

To append all elements from one array onto another, use the spread operator:

javascriptjavascript
const existing = [1, 2, 3];
const additional = [4, 5, 6];
 
existing.push(...additional);
console.log(existing); // [1, 2, 3, 4, 5, 6]
Spread Limit

Using push(...largeArray) with extremely large arrays (100,000+ elements) can hit the JavaScript engine's maximum call stack size. For very large merges, use a loop or Array.prototype.concat() instead.

Return Value Trap

The most common push() mistake is assuming it returns the modified array:

javascriptjavascript
// Bug: push() returns the new length, not the array
const result = [1, 2, 3].push(4);
console.log(result); // 4 (the length, not the array!)
 
// What the developer expected:
// [1, 2, 3, 4]
 
// Fix: if you need the array back, use spread
const arr = [1, 2, 3];
const withFour = [...arr, 4]; // [1, 2, 3, 4]

The pop() Method

pop() removes the last element from an array and returns that element:

javascriptjavascript
const stack = ["A", "B", "C", "D"];
 
const removed = stack.pop();
console.log(removed); // "D"
console.log(stack);   // ["A", "B", "C"]

Syntax

javascriptjavascript
const removedElement = array.pop()

Popping from an Empty Array

Calling pop() on an empty array returns undefined without error:

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

push() and pop() at a Glance

Propertypush()pop()
ActionAdds element(s) to endRemoves last element
ParametersOne or more valuesNone
ReturnsNew array lengthRemoved element
Mutates originalYesYes
Time complexityO(1) amortizedO(1)
On empty arrayAdds normallyReturns undefined

Building a Stack with push() and pop()

A stack follows Last-In-First-Out (LIFO) order, where the most recent addition is the first to be removed. push() and pop() naturally implement this pattern:

javascriptjavascript
class BrowserHistory {
  constructor() {
    this.pages = [];
  }
 
  visit(url) {
    this.pages.push(url);
    console.log(`Navigated to: ${url}`);
  }
 
  back() {
    if (this.pages.length === 0) {
      console.log("No history to go back to");
      return null;
    }
    const previous = this.pages.pop();
    console.log(`Going back from: ${previous}`);
    return previous;
  }
 
  current() {
    return this.pages.at(-1) || "No page loaded";
  }
}
 
const history = new BrowserHistory();
history.visit("https://example.com");
history.visit("https://example.com/about");
history.visit("https://example.com/contact");
 
console.log(history.current()); // "https://example.com/contact"
history.back();                  // "Going back from: https://example.com/contact"
console.log(history.current()); // "https://example.com/about"

Real-World Patterns

Undo/Redo System

javascriptjavascript
class UndoManager {
  constructor() {
    this.history = [];
    this.redoStack = [];
  }
 
  execute(action) {
    action.do();
    this.history.push(action);
    this.redoStack.length = 0; // Clear redo after new action
  }
 
  undo() {
    const action = this.history.pop();
    if (!action) return;
    action.undo();
    this.redoStack.push(action);
  }
 
  redo() {
    const action = this.redoStack.pop();
    if (!action) return;
    action.do();
    this.history.push(action);
  }
}
 
// Usage with a text editor
const manager = new UndoManager();
let text = "";
 
manager.execute({
  do() { text += "Hello"; },
  undo() { text = text.slice(0, -5); },
});
console.log(text); // "Hello"
 
manager.execute({
  do() { text += " World"; },
  undo() { text = text.slice(0, -6); },
});
console.log(text); // "Hello World"
 
manager.undo();
console.log(text); // "Hello"
 
manager.redo();
console.log(text); // "Hello World"

Collecting Form Entries

javascriptjavascript
const formEntries = [];
 
function addEntry(fieldName, value) {
  if (!fieldName || value === undefined) {
    throw new Error("Field name and value are required");
  }
 
  formEntries.push({
    field: fieldName,
    value: value,
    timestamp: Date.now(),
  });
}
 
function removeLastEntry() {
  const removed = formEntries.pop();
  if (removed) {
    console.log(`Removed entry: ${removed.field} = ${removed.value}`);
  }
  return removed;
}
 
addEntry("name", "Alice Johnson");
addEntry("email", "alice@example.com");
addEntry("role", "Developer");
console.log(formEntries.length); // 3
 
removeLastEntry(); // "Removed entry: role = Developer"
console.log(formEntries.length); // 2

Batch Processing Queue

Processing items in batches using pop() to drain a work list:

javascriptjavascript
async function processInBatches(items, batchSize, processFn) {
  const queue = [...items]; // Clone to avoid mutating input
  const results = [];
 
  while (queue.length > 0) {
    const batch = [];
    for (let i = 0; i < batchSize && queue.length > 0; i++) {
      batch.push(queue.pop());
    }
 
    const batchResults = await Promise.all(batch.map(processFn));
    results.push(...batchResults);
    console.log(`Processed batch. Remaining: ${queue.length}`);
  }
 
  return results;
}
 
// Example usage
const urls = Array.from({ length: 20 }, (_, i) => `https://api.example.com/item/${i}`);
// processInBatches(urls, 5, fetchData);

push() and pop() vs Alternatives

TaskMutation (push/pop)Immutable alternative
Add to endarr.push(x)[...arr, x]
Remove from endarr.pop()arr.slice(0, -1)
Add to startunshift()[x, ...arr]
Remove from startshift()arr.slice(1)

When should you choose mutation over immutable patterns?

Use push()/pop() when:

  • You own the array and no other code references it
  • Performance matters (mutation is faster than creating new arrays)
  • You are building an internal data structure (stack, queue, buffer)

Use spread/slice when:

  • The array is shared state (React state, Redux store, function arguments)
  • You need the original array preserved
  • You are chaining transformations

Performance Deep Dive

Both push() and pop() operate in O(1) amortized time. This means individual calls complete in constant time, with occasional pauses when the engine needs to resize the underlying storage.

javascriptjavascript
// Benchmark: push 1 million elements
const arr = [];
console.time("push-million");
for (let i = 0; i < 1_000_000; i++) {
  arr.push(i);
}
console.timeEnd("push-million"); // ~15-25ms in V8
 
// Benchmark: pop all 1 million elements
console.time("pop-million");
while (arr.length > 0) {
  arr.pop();
}
console.timeEnd("pop-million"); // ~8-15ms in V8

Compare this to unshift() which must re-index every element:

Operation10 items10,000 items1,000,000 items
push()~0.001ms~0.5ms~20ms
pop()~0.001ms~0.3ms~12ms
unshift()~0.001ms~2ms~500ms+
shift()~0.001ms~1.5ms~400ms+

Common Mistakes

Chaining on push() (returns length, not array):

javascriptjavascript
// Bug: push returns the new length
const result = [].push(1).push(2); // TypeError: (3).push is not a function
 
// Fix: chain properly
const arr = [];
arr.push(1);
arr.push(2);
// Or use a builder pattern: [...[], 1, 2]

Mutating state in React:

javascriptjavascript
// Bug: React won't re-render because the reference hasn't changed
function addTodo(todo) {
  todos.push(todo); // Mutates existing array
  setTodos(todos);  // Same reference — React skips re-render
}
 
// Fix: create a new array
function addTodo(todo) {
  setTodos([...todos, todo]); // New array — React detects change
}

Forgetting pop() returns the removed element:

javascriptjavascript
const stack = [10, 20, 30];
 
// Developer forgot to capture the return value
stack.pop();
// The value 30 is gone forever
 
// Correct: capture it if you need it
const last = stack.pop();
console.log(last); // 20
Rune AI

Rune AI

Key Insights

  • push() adds to end, pop() removes from end: Both are O(1) and mutate the original array
  • Return values matter: push() returns the new length; pop() returns the removed element
  • Stack pattern: push() and pop() naturally implement LIFO (Last-In-First-Out) data structures
  • Mutation awareness: Never use push()/pop() on shared state or React state; use spread or slice() instead
  • Performance advantage: End operations are dramatically faster than start operations (shift()/unshift()) on large arrays
RunePowered by Rune AI

Frequently Asked Questions

Does push() return the new array?

No. `push()` returns the new length of the array as a number, not the modified array. This is a common source of bugs when developers try to chain `push()` calls. If you need the array, use the spread operator: `const newArr = [...arr, newElement]`.

What is the difference between push() and concat()?

`push()` mutates the original array and adds individual elements. `concat()` creates and returns a new array without modifying either input. Use `push()` for internal data structures and `concat()` or spread when you need immutability. `concat()` also accepts arrays as arguments and flattens them one level.

Can push() add multiple elements at once?

Yes. Pass as many arguments as needed: `arr.push(1, 2, 3)`. All elements are added in order. To push all elements from another array, use spread: `arr.push(...otherArray)`, but watch for stack overflow on very large arrays.

Is pop() faster than shift()?

Yes, significantly. `pop()` runs in O(1) time because it removes the last element without affecting other indices. `shift()` runs in O(n) time because every remaining element must be re-indexed (index 1 becomes 0, index 2 becomes 1, and so on). For large arrays, this difference is dramatic.

How do I pop multiple elements at once?

There is no built-in method to pop N elements. Use `splice()` instead: `const removed = arr.splice(-n, n)` removes the last `n` elements and returns them as an array. Alternatively, use `arr.slice(-n)` to get the last N elements without mutation, then `arr.length -= n` to truncate.

Conclusion

push() and pop() are the workhorses of array manipulation in JavaScript. They operate at the end of the array in O(1) time, making them ideal for stacks, buffers, history tracking, and any pattern where you process data in LIFO order. The critical details to remember are that push() returns the new length (not the array), both methods mutate the original, and mutation-based patterns should be replaced with spread or slice when working with shared state in frameworks like React.