
Mutation vs. Immutability in JavaScript Arrays and Objects

Mutation and immutability sound like grand theoretical topics until a perfectly ordinary bit of JavaScript quietly changes the wrong array and leaves you wondering why two parts of the UI suddenly disagree with each other.
That is when the subject becomes practical very quickly.
New developers usually meet this problem through shared arrays and objects. They copy a reference by accident, change one thing in one place, and then discover that the original data has changed too. Or they use a method like splice() or sort() assuming it returns a new array, only to realise later that it changed the existing one in place.
These are not exotic bugs. They are some of the most common JavaScript data bugs around.
Mutation Means Changing the Original Value
If you mutate an array or object, you change the existing value directly.
const numbers = [1, 2, 3];numbers.push(4);console.log(numbers);After that code runs, numbers is [1, 2, 3, 4]. The original array itself has changed.
Objects behave the same way:
const user = { name: 'Sophie', role: 'Editor',};user.role = 'Admin';That is mutation as well. The same object now contains different data.
Immutability Means Creating a New Value Instead
Immutability in ordinary JavaScript does not mean data becomes physically impossible to change. It usually means you choose not to change the original value. Instead, you create a new array or object containing the updated data.
For example:
const numbers = [1, 2, 3];const nextNumbers = [...numbers, 4];Now the original numbers array is unchanged, and nextNumbers is the new version.
For objects:
const user = { name: 'Sophie', role: 'Editor',};const nextUser = { ...user, role: 'Admin',};That is the core idea. Instead of changing shared data in place, you produce an updated copy.
Why Beginners Get Caught by This
JavaScript arrays and objects are reference types. That is where much of the confusion comes from.
If you do this:
const original = ['a', 'b', 'c'];const copy = original;copy.push('d');you have not really created a new array. copy and original both point to the same underlying array. So when you mutate through copy, original changes too.
That is why the result is:
console.log(original); // ['a', 'b', 'c', 'd']console.log(copy); // ['a', 'b', 'c', 'd']This is one of those bugs that makes perfect sense once you know the rule and feels ridiculous before that.
Some Array Methods Mutate and Some Do Not
This is a list worth learning because it saves a lot of trouble.
Common mutating array methods include:
pushpopshiftunshiftsplicesortreverse
Common non‑mutating array methods include:
mapfiltersliceconcat
That distinction matters because methods with similar‑looking jobs can behave very differently.
For example:
const items = ['a', 'b', 'c'];const result = items.slice(1);slice returns a new array and leaves items alone.
But:
const items = ['a', 'b', 'c'];const result = items.splice(1, 1);changes items directly.
That one pair alone causes a lot of confusion because the names look similar enough to blur together when you are learning quickly.
Why Immutability Helps in Real Projects
Immutability is not valuable because it sounds tidy. It is valuable because it makes state changes easier to reason about.
If a function receives some data and returns new data without changing the input, you can usually trust that other parts of the application are not going to be surprised by hidden side effects.
That helps with:
- debugging
- testing
- UI state updates
- predictable change detection
If you are working with a front‑end framework, this matters even more because many rendering patterns rely on comparing old and new values. Quiet in‑place mutation makes those updates harder to track.
Even outside frameworks, immutable updates make code easier to read because the update is explicit. You can see the old value and the new value as two separate things.
But Immutability is Not the Same as Deep Safety
This is where people can get overconfident.
Using the spread operator or slice() often gives you a new top‑level array or object, but nested values can still be shared.
const user = { name: 'Sophie', preferences: { theme: 'dark', },};const nextUser = { ...user,};nextUser.preferences.theme = 'light';That changes user.preferences.theme as well, because the nested preferences object was not copied deeply. Both objects still point at the same nested value.
That is why shallow copies are useful but not magical. They solve one layer of mutation, not every possible layer.
When Mutation is Perfectly Fine
This is worth saying as well, because beginners can swing too hard the other way and start treating all mutation as if it were morally wrong.
Mutation is often fine when:
- the data is local and not shared
- the scope is small and obvious
- performance matters and the trade‑off is clear
- the code is clearer with a direct update
For example, mutating a local array inside a small utility can be perfectly reasonable if nothing else depends on the previous value.
The real danger is not mutation by itself. It is surprising mutation of shared state.
A Practical Pattern for Safer Updates
For arrays, these patterns are often clearer:
const addItem = (items: string[], item: string): string[] => { return [...items, item];};const removeItem = (items: string[], itemToRemove: string): string[] => { return items.filter((item) => item !== itemToRemove);};For objects:
type User = { name: string; role: string;};const updateRole = (user: User, role: string): User => { return { ...user, role, };};These patterns are not only safer. They also make the update intent very obvious.
The Best Question to Ask
When you are deciding between mutation and immutability, the best question is usually:
"Who else can see this value?"
If the answer is "lots of other code" or "the UI depends on it in several places", immutable updates are often the safer option.
If the answer is "only this tiny local block", mutation may be completely fine.
That framing is more useful than treating the topic as ideology.
Wrapping up
Mutation changes the original array or object. Immutability creates a new version instead. In JavaScript, bugs usually appear when developers think they have created a copy but have actually kept a shared reference, or when they use a mutating array method without realising it. Once you know which methods mutate and when shared references are in play, these problems become much easier to avoid.
Key Takeaways
- Arrays and objects are reference types, so assigning them does not create a true copy.
- Mutating methods such as
push,splice,sort, and direct property assignment change the original value. - Immutable updates create a new array or object instead of changing the existing one.
- Shallow copies help, but nested objects can still be shared.
- Mutation is not automatically bad; surprising mutation of shared state is the real problem.
Once you can spot where data is shared and where it is safe to update in place, a lot of awkward JavaScript bugs start to disappear.
Related Articles

Array.includes() vs. indexOf() in JavaScript. 
Pass by Value vs. Reference in JavaScript. Pass by Value vs. Reference in JavaScript

Removing p Tags from Contentful List Items. Removing
pTags from Contentful List Items
Margin Collapse in CSS. Margin Collapse in CSS

Lazy Loading in Angular: Optimising Performance. Lazy Loading in Angular: Optimising Performance

Array.find(), Array.some(), and Array.every() in JavaScript. Array.find(),Array.some(), andArray.every()in JavaScript
Ethical Web Development ‑ Part I. Ethical Web Development ‑ Part I

Interpolation: Sass Variables Inside calc(). Interpolation: Sass Variables Inside
calc()
How to Import All Named Exports from a JavaScript File. How to Import All Named Exports from a JavaScript File

React's Reconciliation Algorithm Explained. React's Reconciliation Algorithm Explained

Understanding setTimeout() in JavaScript. Understanding
setTimeout()in JavaScript
Dynamic Calculations in CSS Using calc(). Dynamic Calculations in CSS Using
calc()