What do JavaScript's in-place array methods such as push, pop, splice, reverse and fill actually return, and what bug follows from assuming they return a new array?
answer
- not one of them returns a fresh array
- three different shapes of return value
- one group hands you the receiver back
- push returns a number, pop returns an element
- b === a after reverse()
basics
~20 sMutating array methods rarely return a new array: push and unshift return the new length, pop and shift return the removed element, splice returns the removed elements, and reverse, sort, fill and copyWithin return the same array object they just changed.
solid answer
~50 sNone of the in-place methods hands back a fresh array. `push` and `unshift` return the new `length`; `pop` and `shift` return the single element they took off; `splice` returns an array of removed elements; and `reverse`, `sort`, `fill` and `copyWithin` return **a reference to the very same array**. That last group is the dangerous one, because `const b = a.reverse()` looks like a copy and even passes a shallow test — but `b === a` is `true` and `a` is now reversed as well. Anyone else holding `a` sees the change. The same shape bites with `const sorted = list.sort(cmp)`. The fix is to copy first — `a.slice().reverse()` — or use the ES2023 copying methods `toReversed`/`toSorted`. And `const arr2 = arr.push(x)` is the beginner version of the same misread: `arr2` ends up being a number.
code
javascript · 11 linesconst a = [3, 1, 2];
console.log(a.push(4)); // 4 <- new length
console.log(a.pop()); // 4 <- removed element
console.log(a.splice(0, 1)); // [3] <- removed elements
const b = a.reverse();
console.log(b === a); // true <- same object, not a copy
const safe = a.slice().reverse();
console.log(safe === a); // false <- copy, then mutate the copygo deeper
Remember that push returns the new length and pop returns the removed element, and that none of these methods gives you a new array to work with.
Lay out the three return shapes, and explain why reverse and sort returning the same reference causes silent bugs while push returning a number fails immediately.
Demonstrate the diagnosis: an unexpected reorder somewhere far from the call site, traced back to an aliased array, and fixed by copying at the boundary rather than patching the symptom.
Speak to prevention at scale — lint rules against in-place methods on shared data, freezing in development builds, and conventions that make ownership of an array explicit in your APIs.
## Three different return-value shapes JavaScript's mutating array methods are not consistent with each other, which is why the question gets asked. There are three groups: **1. Return a count.** `push(...items)` and `unshift(...items)` return the array's **new length** after the insertion. ```js const a = [1, 2]; console.log(a.push(3)); // 3 <- length, not the array console.log(a.unshift(0)); // 4 ``` **2. Return what came out.** `pop()` and `shift()` return the single removed element (or `undefined` on an empty array); `splice(start, deleteCount, ...items)` returns an **array** of the removed elements, possibly empty. ```js const b = ['x', 'y', 'z']; console.log(b.pop()); // 'z' console.log(b.shift()); // 'x' console.log(b.splice(0, 1)); // ['y'] ``` **3. Return the same array object.** `reverse()`, `sort()`, `fill(value, start, end)` and `copyWithin(target, start, end)` mutate the receiver and then return **a reference to it** — not a copy. ```js const c = [1, 2, 3]; const d = c.reverse(); console.log(d); // [3, 2, 1] console.log(c); // [3, 2, 1] — also reversed console.log(d === c); // true — one array, two names ``` ## Why group 3 is the real trap Groups 1 and 2 fail loudly: if you write `const list = arr.push(x)` you get a number and the next line explodes. Group 3 fails **quietly**, because the returned value is an array with exactly the contents you expected. The bug only surfaces when someone else reads the original: ```js const original = [3, 1, 2]; const display = original.reverse(); // later, somewhere else render(original); // shows [2, 1, 3] — nobody asked for this ``` This is aliasing: two bindings, one object. `const` does not help — it freezes the binding, not the array's contents, so `const original` can still be reordered in place. ## Why the API is shaped this way Returning the receiver enables chaining: `arr.fill(0).reverse()` works because each call passes the same array along. It was a deliberate convenience that predates the modern preference for non-mutating pipelines, and it is exactly why `map(...).filter(...)` feels safe while `reverse().slice()` does not — the first pair creates new arrays at every step, the second reorders the source before copying. ## Recognising and fixing it The reliable habit is: **if the method name is a verb that describes changing the array, assume it mutates.** `push`, `pop`, `shift`, `unshift`, `splice`, `sort`, `reverse`, `fill`, `copyWithin` all mutate. The copying set is `slice`, `concat`, `map`, `filter`, `flat`, `flatMap`, `join` (returns a string), and the ES2023 additions `toSorted`, `toReversed`, `toSpliced`, `with`. To get a reversed or reordered array without disturbing the source: ```js const reversed = arr.slice().reverse(); // copy, then mutate the copy const reversed2 = arr.toReversed(); // ES2023, one call ``` Both are shallow copies, which is fine here because reordering only touches the outer array. ## A note on frozen arrays If the array has been frozen with `Object.freeze`, a mutating method throws a `TypeError` in strict mode (which includes all module code) rather than silently doing nothing. Freezing is therefore a way to make accidental mutation loud during development. ## What a strong answer sounds like Name the three return shapes, single out the same-reference group as the one that causes silent bugs, show the `const b = a.reverse()` aliasing example, and give both fixes — copy-then-mutate, or the ES2023 copying method.
- Why doesn't declaring the array with const prevent this mutation?`const` makes the *binding* immutable, not the value. You cannot reassign `arr` to a different array, but the array object itself stays fully mutable, so `arr.push(x)` and `arr.reverse()` both work. To make the contents resist change you need `Object.freeze(arr)`, which makes mutating methods throw in strict mode.
- What does pop() return on an empty array?`undefined`, and `length` stays `0`. `shift()` behaves the same way. That means you cannot distinguish "the array was empty" from "the last element happened to be `undefined`" by the return value alone — check `length` first if the difference matters.
- Is arr.fill(0).reverse() safe to chain?It runs, because `fill` returns the same array so `reverse` has something to work on — but both calls mutate `arr` itself. The chain reads like a pipeline and behaves like two in-place edits, which is precisely the misreading to avoid when the array is shared.
saying these in an interview costs you the question
- Saying push returns the array so you can chain it
- Thinking const arr prevents push and reverse
- Believing reverse() hands back a copy
- Assuming pop returns the shortened array
- Expecting splice to return the array after editing