Mutation vs. Immutability in JavaScript Arrays and Objects

Hero image for Mutation vs. Immutability in JavaScript Arrays and Objects. Image by Hulki Okan Tabak.
Hero image for 'Mutation vs. Immutability in JavaScript Arrays and Objects.' Image by Hulki Okan Tabak.

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:

  • push
  • pop
  • shift
  • unshift
  • splice
  • sort
  • reverse

Common nonmutating array methods include:

  • map
  • filter
  • slice
  • concat

That distinction matters because methods with similarlooking 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 frontend framework, this matters even more because many rendering patterns rely on comparing old and new values. Quiet inplace 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 toplevel 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 tradeoff 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.


Categories:

  1. Development
  2. Front‑End Development
  3. Guides
  4. JavaScript