Immutability is a powerful concept in programming and especially important in JavaScript, even if we don't always think about it. It affects how we manage data, state and side effects in our applications.
In this post, we'll break down what immutability means, why it matters and how you can work with it in practice.
What is Immutability?
Basically Immutability means something cannot be changed after it's created. For example in JavaScript, primitive values like numbers, strings and booleans are immutable. This doesn't mean the variable can't be reassigned, it means the value itself can't be changed in place.
Example with a string:
let name = 'Matsu';
// Trying to change just one character (this won't work)
name[0] = 'B';
console.log(name); // 'Matsu' → Still the same
// But we can reassign the variable
name = 'John';
console.log(name); // 'John'
So the string itself is immutable but the variable name can be reassigned to a new string. In contrast, objects and arrays are mutable by default, which means you can change their contents directly.
Example with an object:
const user = { name: 'Matsu' };
// Modifying the object
user.name = 'John';
console.log(user); // { name: 'John' }
This can lead to unintended side effects, especially when multiple parts of your code are referencing and modifying the same object or array, such as shared data or state.
What Can Immutability Do?
- Avoid side effects (accidentally changing shared data);
- Make debugging easier (state doesn't change in hidden ways);
- Improve performance in some cases (e.g. with reference checks);
- Work better with frameworks like React, which rely on state changes via new objects;
Working with Immutability in JavaScript
To keep data immutable, we usually create new copies instead of modifying the original.
Copying Arrays
const numbers = [1, 2, 3];
// Instead of: numbers.push(4)
const newNumbers = [...numbers, 4];
console.log(numbers); // [1, 2, 3]
console.log(newNumbers); // [1, 2, 3, 4]
Copying Objects
const user = { name: 'Matsu', age: 28 };
// Instead of: user.age = 29
const updatedUser = { ...user, age: 29 };
console.log(user); // { name: 'Matsu', age: 28 }
console.log(updatedUser); // { name: 'Matsu', age: 29 }
Attention with Deep Copy vs Shallow Copy
Most techniques (like spread (...
) and Object.assign
) create shallow copies. This means only top-level properties are copied. If the object contains nested objects, those nested references remain shared between the original and the copy.
const person = {
name: 'Matsu',
address: { city: 'Tokyo' }
};
const clone = { ...person };
clone.address.city = 'Osaka';
console.log(person.address.city); // 😱 'Osaka'
Even though we used the spread operator, person.address
and clone.address
still point to the same object in memory, so changing one affects the other.
To truly copy all levels of an object, you need a deep clone. One common approach is using cloneDeep
from Lodash:
import _ from 'lodash';
const deepClone = _.cloneDeep(person);
This creates a new object where even the nested structures are fully cloned, breaking the reference between the original and the copy.
You can check my post about Exploring Object Cloning Techniques in JavaScript to dive deeper.
Enforcing Immutability in JavaScript
You might think using const
would make an object or array immutable, but that's not the case. const
only prevents reassignment, not mutation.
By default, JavaScript objects and arrays are mutable. But there are ways to partially enforce immutability at runtime using built-in methods like:
Object.freeze()
Prevents adding, removing or modifying properties of an object making the object shallowly immutable.
const config = Object.freeze({
apiUrl: 'https://api.example.com',
retries: 3,
});
config.retries = 5;
console.log(config.retries); // 3 — change is ignored or throws in strict mode
⚠️ Note: This only works on the first level. If the object has nested properties, they’re still mutable unless you recursively freeze them.
Example: Freezing an Array
You can also use Object.freeze()
to prevent changes to an array, for example, if you want to enforce a fixed set of values (e.g. length, content):
const roles = Object.freeze(['admin', 'editor', 'viewer']);
roles.push('guest'); // ❌ won't work
roles[0] = 'superadmin'; // ❌ won't work
roles.length = 0; // ❌ won't work
console.log(roles); // [ 'admin', 'editor', 'viewer' ]
This is useful when you want to protect reference lists, roles, or static options in your app.
Object.seal()
Prevents adding or removing properties, but you can still modify existing ones.
const settings = { theme: 'dark' };
Object.seal(settings);
settings.theme = 'light'; // ✅ allowed
settings.language = 'en'; // ❌ can't add new property
delete settings.theme; // ❌ can't delete property
console.log(settings); // { theme: 'light' }
These methods are great for protecting config objects, shared constants or preventing accidental mutations especially in larger codebases.
When to Use Immutability?
- When managing application state (especially in React or Redux);
- When writing pure functions;
- When you want predictable and reliable behavior in your code;
Final Thoughts
Immutability is not about never changing anything, it's about changing things safely by creating new versions instead of modifying existing ones. You can also "lock" objects and arrays using Object.freeze()
or Object.seal()
to help prevent accidental changes.
It leads to better, cleaner and more bug-resistant code, especially as your project grows. So embrace immutability and you'll avoid a lot of hidden side effects and unexpected behavior.
Console You Later!