Adding an item to an array looks harmless. If that array was handed to three parts of the program, all three see the change, without asking for it.
That is what mutation does: it produces no new value, it modifies the one that already exists, for everyone holding it.
Definition
A mutation is a change applied in place to an existing value. Only objects and arrays can mutate, because they are handled by reference. Numbers and strings never change.
const original = [3, 1, 2];
const assumedCopy = original;
assumedCopy.push(4);
console.log(original); // [ 3, 1, 2, 4 ]
const realCopy = [...original];
realCopy.push(5);
console.log(original.length); // 4
console.log(realCopy.length); // 5assumedCopy is not a copy: it is a second name for the same array. Spread (...) builds a real, independent one.
Array methods, sorted into two camps
The confusion comes from the two families looking alike when written. This two-column table separates them.
| Modify the array | Return a new array |
|---|---|
push, pop, shift, unshift | concat, slice |
splice | filter, toSpliced |
sort | toSorted |
reverse | toReversed |
The three methods starting with to were added to the language precisely to offer a version that does not mutate. See Array.sort() and slice() for the details of each.
When mutating is still the right call
- On a local value. An array built inside a function and never handed out risks nothing.
- On very large volumes. Copying a million items at every step eventually shows.
- Inside an explicit accumulator. A Array.reduce() filling its own object stays readable and safe.
The practical rule fits in one sentence: mutate what you created, never what you received. A modified argument is a Side effect the caller never saw coming.
Frequently asked questions
How do I track down a stray mutation?
The classic symptom is a value that changes while no visible line touches it. Freezing the structure with Object.freeze() during development surfaces the culprit, since the change then throws instead of slipping through. See Immutability for the limits of freezing.
Can a string mutate?
No. Every String method returns a new string, including the ones that look like they transform the original. Writing text.toUpperCase() without capturing the result changes nothing. The same holds for numbers and booleans.
Why does my sort modify the original array?
Because sort() sorts in place and returns the same array, not a copy. It is the most common trap of the family, especially when the result is assigned to a new variable, which creates the illusion of a copy. Use toSorted(), or sort a copy obtained with [...list].