A helper function receives an array parameter and calls reverse() on it before returning a slice. What goes wrong for the caller, and how should the function be written instead?
answer
- the parameter is not a copy of the array
- the return value is right, the input isn't
- calling it twice gives a different answer
- reverse edits the caller's data
- copy at the boundary before reordering
basics
~20 sCalling an in-place method such as reverse, sort, splice or fill on an array parameter edits the caller's own array, causing changes the caller never asked for. Copy first — with slice() or an ES2023 copying method — before reordering shared input.
solid answer
~50 sArray arguments are passed as references, so the parameter names the caller's array, not a copy. `items.reverse()` inside the function permanently reorders the array the caller still holds, and every later reader of it sees the new order. Worse, the function looks correct in isolation: it returns exactly the right slice, so a unit test that only checks the return value passes. The bug shows up as data mysteriously changing order somewhere far from this code, and it compounds when the function is called twice — the second call reverses an already-reversed array. The fix is a one-word change: copy before you mutate, `items.slice().reverse().slice(0, 3)`, or in an ES2023 runtime `items.toReversed().slice(0, 3)`. The general rule is that a function should not mutate arguments it does not own, and if it deliberately does, that must be obvious in its name and documented.
code
javascript · 16 linesfunction newestBuggy(items) {
return items.reverse().slice(0, 2);
}
function newestSafe(items) {
return items.slice().reverse().slice(0, 2);
}
const log = ['a', 'b', 'c'];
console.log(newestBuggy(log)); // ['c', 'b']
console.log(log); // ['c','b','a'] — caller's array reordered
console.log(newestBuggy(log)); // ['a', 'b'] — different answer, same input
const log2 = ['a', 'b', 'c'];
console.log(newestSafe(log2)); // ['c', 'b']
console.log(log2); // ['a','b','c'] — untouchedgo deeper
Know that an array argument refers to the caller's array, so calling reverse or sort on a parameter changes data outside the function.
Explain why the chain reads as pure but is not, show the copy-first fix with slice or toReversed, and point out the function is not idempotent across repeated calls.
Demonstrate the diagnostic path from a far-away symptom back to the mutating callee, and the defences: assert the input in tests, freeze in development, and copy at API boundaries by default.
Set the contract across the codebase — which layers may take ownership of data, how in-place routines are named and documented, and where copying costs are acceptable versus measured out on hot paths.
## Why the parameter is not a copy When you pass an array to a function, the reference is copied but the array is not. The parameter binding is a second name for the caller's array object, so any in-place operation performed through it is performed on the caller's data. ```js function newest(items) { return items.reverse().slice(0, 3); // BUG } const log = ['a', 'b', 'c', 'd']; console.log(newest(log)); // ['d', 'c', 'b'] console.log(log); // ['d','c','b','a'] — the caller's array is reordered ``` Reassigning the parameter (`items = something`) would not affect the caller, because that rebinds a local name. Mutating through it does. ## Why this class of bug survives review and tests Three things make it hard to catch: 1. **The return value is correct.** A test that asserts on the result passes. Only a test that also asserts the *input is unchanged* catches it. 2. **`reverse` returns the array**, so the chain `items.reverse().slice(0, 3)` reads like a pipeline of pure steps. Nothing in the syntax hints that the first step edited the source. 3. **The damage is non-local.** The symptom appears in whatever code reads the array afterwards, which may be a different module entirely. And because the mutation is cumulative, the function is not even idempotent: calling `newest(log)` twice returns different answers, since the second call reverses the already-reversed array. That produces the maddening "it's wrong only the second time" bug report. ## The fix Copy at the top of the function, then mutate the copy freely: ```js function newest(items) { return items.slice().reverse().slice(0, 3); } // ES2023 function newestModern(items) { return items.toReversed().slice(0, 3); } ``` The same discipline applies to `sort`, `splice`, `fill`, `copyWithin`, `push` and `pop` on an argument. A shallow copy is enough here because reordering and truncating only touch the outer array; if the function also edited properties on the elements, it would need to copy those too. ## When mutating an argument is legitimate Sometimes in-place is the point — a function whose job is to sort a large buffer without allocating a second one, or a low-level routine on a hot path where the copy is measurable. Two rules make that acceptable: **say so in the name** (`sortInPlace(buffer)`, `drainInto(target)`) and **document the ownership transfer**. The failure mode is not mutation itself; it is mutation that the call site cannot see. ## Making it visible Three practical defences, in increasing strength: - **Test the input.** Assert after the call that the argument still equals what it was. This is the single highest-value habit, because it turns an invisible bug into a failing test. - **Freeze during development.** `Object.freeze(arr)` before passing it makes any in-place method throw a `TypeError` in strict mode (all module code is strict), pointing straight at the offending line. - **Prefer copying methods by default** in shared code, so the safe path is also the one you type without thinking. ## The one-line rule to state in an interview "A function should treat its arguments as read-only unless mutating them is its declared job." Then show that you know which array methods break that rule — `push`, `pop`, `shift`, `unshift`, `splice`, `sort`, `reverse`, `fill`, `copyWithin` — and which ES2023 copying methods let you avoid the copy-then-mutate dance.
- How would you write a test that catches this bug?Assert on the input as well as the output: capture the argument's contents before the call and check they are unchanged afterwards, for example comparing against a literal or a pre-made copy. A stronger variant is to freeze the array before passing it, so the in-place call throws instead of silently succeeding.
- Does the same risk exist when the function only reads elements and calls map?No. `map`, `filter`, `slice` and `concat` build new arrays and never write to the receiver, so the caller's array is safe. The risk is specific to the in-place set — push, pop, shift, unshift, splice, sort, reverse, fill and copyWithin — plus direct index assignment on the parameter.
- When is mutating the argument the right design?When avoiding the allocation is the point — sorting a large buffer in place on a measured hot path, or a routine that is explicitly given ownership of a scratch array. Make it visible: name the function so the mutation is obvious, and document that the caller must not reuse the array afterwards.
saying these in an interview costs you the question
- Thinking arrays are passed by value into functions
- Believing a const parameter blocks mutation
- Testing only the return value and calling it verified
- Claiming reverse() returns a copy so the source is safe
- Deep-cloning every argument instead of copying the level touched