JAVASCRIPT / ARRAYS
Push, pop, shift, and unshift at the ends
Add and remove elements at both ends of an array with push, pop, shift, and unshift, and read their return values correctly.
What you will learn
- push and pop act on the last index; unshift and shift act on index 0
- Read return values: push/unshift give the new length, pop/shift the removed item
- pop and shift on an empty array return undefined instead of throwing
- Model push/pop as a stack and push/shift as a queue over one array
Understanding Push, pop, shift, and unshift at the ends
These four methods sit on two axes: which end they touch, and whether they add or remove. push and pop work at the tail, the slot at index length - 1; unshift and shift work at the head, index 0. All four change the array in place and adjust length as a side effect, so none of them hands you a new array. Their return values are what people trip over: the two adders return the new length, and the two removers return the element they took out.
The head pair behaves differently for a structural reason. An array is a numbered mapping from 0 up to length - 1, so writing or clearing the tail slot only disturbs one number plus length. Removing at the head means every surviving element has to move down one index, and inserting at the head means every element moves up one, which is work proportional to the array's size and also invalidates any index you were holding for a specific element. That is why push and pop are the cheap default and why shift in a loop over a large array quietly turns into quadratic work.
Once the return values are clear, the pairs compose into familiar shapes: push with pop gives last-in-first-out, push with shift gives first-in-first-out, and unshift with pop gives the same queue running the other direction. Because they mutate, they are the right tool when you own the array and the wrong tool when a caller also holds a reference to it; for that case the non-mutating equivalents are [...arr, x] and [x, ...arr]. Note that const does not protect you here: const freezes the binding, not the contents, so pushing into a const array is perfectly legal.
const letters = [];
console.log(letters.push("a")); // adders return the new length
console.log(letters.push("b", "c")); // push accepts any number of values
console.log(JSON.stringify(letters));
console.log(letters.pop()); // removers return the removed value
console.log(letters.unshift("z")); // inserted at index 0
console.log(JSON.stringify(letters));
console.log(letters.shift());
console.log(letters.length);
console.log(letters.pop(), letters.pop(), letters.pop());All four methods mutate the array in place and return a number or the removed element rather than the array, and only the head pair renumbers the elements that stay.
Worked examples
Same additions, two removal orders
Removing from the tail gives last-in-first-out, removing from the head gives first-in-first-out.
const tasks = [];
tasks.push("write");
tasks.push("review");
tasks.push("ship");
const newest = tasks.pop();
const oldest = tasks.shift();
console.log(newest, oldest);
console.log(JSON.stringify(tasks));Example explained
Line 1Each push appends at index tasks.length, so the array grows at the tail in arrival order.
Line 2pop() returns "ship", the most recently added item, which is what makes push plus pop a stack.
Line 3shift() returns "write", the earliest item, which makes push plus shift a queue.
Line 4Both removals hit the same array, so tasks is left holding only "review".
shift renumbers, pop does not
Shows that a saved index survives a pop but points at the wrong element after a shift.
const queue = ["a", "b", "c", "d"];
console.log(queue[0], queue[3]);
queue.shift();
console.log(queue[0], queue[3], queue.length);
queue.pop();
console.log(queue[0], queue.length);Example explained
Line 1Before any change the indices run 0 to 3, so queue[3] is "d".
Line 2After shift() every remaining element slid down one slot: "b" is now at index 0 and length is 3, so queue[3] reads undefined.
Line 3pop() removed the tail instead, leaving index 0 untouched, which is why queue[0] is still "b".
Line 4The lesson for code: indices you cached stay valid across pop but not across shift or unshift.
The return value is a number
Demonstrates that push returns the new length, mutates the original, and therefore cannot be chained.
const scores = [10, 20];
const returned = scores.push(30);
console.log(returned);
console.log(JSON.stringify(scores));
const copy = [...scores, 40];
console.log(JSON.stringify(copy), JSON.stringify(scores));
try {
[1, 2].push(3).push(4);
} catch (err) {
console.log(err.name);
}Example explained
Line 1returned is 3, the length after the insertion, not the array and not the value 30.
Line 2scores changed in place, so every other reference to that array now sees the extra element.
Line 3[...scores, 40] builds a separate array and leaves scores alone, which is the non-mutating counterpart of push.
Line 4The chain fails because the first push evaluates to the number 3, and numbers have no push method.
Important notes
pop and shift on an empty array return undefined and leave length at 0 rather than throwing, so if (arr.pop()) cannot tell "array was empty" apart from a falsy element like 0 or "" — test length instead.
unshift and shift must renumber the surviving elements, so their cost grows with array size; push and pop touch a single slot and stay cheap regardless of length.
Common mistakes
Writing const bigger = arr.push(item) and then calling bigger.map(...): push handed back a number, so you get a TypeError instead of an array.
Calling arr.pop() or arr.shift() inside for (let i = 0; i < arr.length; i++): length shrinks and indices slide every iteration, so the loop skips elements or stops halfway.
Passing a whole array to push, as in arr.push(extras): that stores one nested array element and grows length by 1, where arr.push(...extras) adds each value separately.
Try it yourself
Change, predict, then run
In a browser console create const log = [] and push three event names onto it, then drain it with shift while printing each removed name and log.length after every removal. Repeat with a fresh array drained by pop and note how the printed order flips.
Open the JavaScript workspaceCheck your understanding
You have const q = ["a", "b", "c"] and a variable i = 2 that was saved because q[2] held "c". After q.shift() runs, what does q[i] evaluate to and why?
- undefined, because shift moved every remaining element down one index and length is now 2
- "c", because shift only removes from the front and leaves the other indices where they were
- "b", because shift rotates the removed value out and pushes the rest toward the end
- Nothing, because shift throws once a previously valid index would point past the array
Show answer
shift removes "a" and renumbers the survivors, so q becomes ["b", "c"] with length 2 and index 2 is past the end, which reads as undefined. Option 2 describes pop, not shift: pop is the operation that leaves indices 0 through length - 2 unchanged, which is exactly why cached indices survive a pop but not a shift.