Tutorial

Mutable Immutable JavaScript

Practical guide to immutability in JavaScript: why it matters, const vs object immutability, Object.freeze vs deep freeze, property descriptors, immutable arrays, Proxy patterns and best practices.

Drake Nguyen

Founder · System Architect

3 min read
Mutable Immutable JavaScript
Mutable Immutable JavaScript

Introduction

Immutability in JavaScript is the practice of preventing unintended changes to data structures. Understanding immutable data JavaScript patterns helps reduce bugs caused by state mutation, makes code easier to reason about, and plays well with functional programming and pure functions.

Mutable vs Immutable: core idea

Mutable vs immutable JavaScript is a common comparison: a mutable value can be changed in place, while an immutable value cannot. Primitive values (numbers, strings, booleans) behave like immutable values, but objects and arrays are passed by reference and are mutable by default.

Example: pass by reference and accidental mutation

// const prevents reassignment but not mutation
const user = { name: 'Alice', address: '123 Lane' };
const copy = user; // reference copy
copy.name = 'Eve';
// user.name === 'Eve' (accidental mutation)

Why use immutability in JavaScript?

  • Avoid side effects and unexpected state changes (state mutation).
  • Make functions predictable and easier to test (pure functions).
  • Simplify debugging and enable safe sharing of values across modules.
  • Improve performance in some frameworks by enabling cheap equality checks.

Tools and techniques

There are several white-hat ways to introduce immutable behavior without adding heavy libraries. Below are practical techniques and examples: const immutability JavaScript, Object.freeze JavaScript, property descriptors JavaScript, and deep freeze JavaScript.

1. const: limited immutability

Using const prevents reassigning the binding, but it does not make objects or arrays immutable.

const arr = [1, 2, 3];
arr[0] = 10; // allowed — arr is still mutable

const obj = { a: 1 };
obj.a = 2; // allowed — property was mutated

Answering the common question: does const make objects immutable in JavaScript? No. const only prevents binding reassignment, not mutation of nested properties.

2. Object.freeze: shallow freeze

Object.freeze JavaScript makes an object non-extensible and its existing properties non-writable and non-configurable — but only at the top level. Nested objects remain mutable.

const user = { name: 'Sam', nested: { age: 30 } };
Object.freeze(user);
user.name = 'Max'; // silently fails in non-strict mode or throws in strict mode
user.nested.age = 31; // still allowed — shallow freeze

3. Deep freeze function (deep freeze JavaScript)

To freeze nested objects recursively you can implement a deep freeze (aka superFreeze / deepFreeze) function.

function deepFreeze(obj) {
  if (obj === null || typeof obj !== 'object') return obj;
  Object.getOwnPropertyNames(obj).forEach(prop => {
    const value = obj[prop];
    if (typeof value === 'object' && value !== null) deepFreeze(value);
  });
  return Object.freeze(obj);
}

const data = { a: { b: 1 } };
deepFreeze(data);
// data.a.b = 2; // now prevented

This deep freeze JavaScript function is a standard approach when you need true immutability without extra libraries.

4. Property descriptors and defineProperty

Using Object.defineProperty you can create read-only properties. Be careful to set writable and configurable to false.

const obj = {};
Object.defineProperty(obj, 'id', {
  value: 42,
  writable: false,        // prevents reassignment
  configurable: false,
  enumerable: true
});

// Attempting to redefine or change obj.id will fail or throw

Example: JavaScript defineProperty writable false configurable false ensures the property cannot be reconfigured or written to, which helps prevent accidental mutation.

5. Immutable arrays: patterns without libraries

Arrays are mutable by default. You can use Object.freeze on arrays or prefer non-mutating methods (map, filter, concat, spread) to produce new arrays instead of changing existing ones.

// Freeze array
const arr = ['a', 'b', 'c'];
Object.freeze(arr);
// arr[0] = 'x'; // prevented

// Non-mutating approach
const newArr = [...arr, 'd']; // immutable arrays JavaScript without libraries

6. Proxy to enforce immutability

A Proxy can intercept attempts to modify objects and throw or ignore changes. This is useful for development-time enforcement.

function immutableProxy(obj) {
  return new Proxy(obj, {
    set(target, prop) {
      throw new Error('Cannot modify immutable object: ' + String(prop));
    },
    deleteProperty() {
      throw new Error('Cannot delete property from immutable object');
    }
  });
}

const p = immutableProxy({ name: 'Zoe' });
// p.name = 'Tom'; // throws

When to use a library

For large applications you may prefer libraries that implement persistent immutable data structures or convenient immutable updates: Immutable.js, Immer, or Ramda. These libraries simplify common patterns and avoid costly deep cloning while keeping code readable.

Best practices and tips

  • Avoid mutating objects JavaScript by default — prefer copying or using non-mutating helpers.
  • Use shallow freeze for small objects; use deep freeze only where performance allows.
  • Prefer immutable arrays JavaScript patterns using spread, slice, map, filter, and concat to return new arrays instead of mutating.
  • Combine property descriptors (defineProperty writable configurable) and Object.freeze when you need read-only API surfaces.
  • Use Proxy traps during development to detect accidental mutation early.

Tip: For predictable state management and fewer side effects, make immutability part of your coding conventions. Small, consistent rules (avoid mutating objects JavaScript, use const, use non-mutating array methods) reduce bugs.

Further reading

  • Compare Object.freeze vs deep freeze JavaScript when deciding between shallow vs recursive immutability.
  • Search for immutable arrays Object.freeze example or const object mutation example to see practical cases.
  • Explore immutability libraries (Immer, Immutable.js) for production-grade patterns.

Conclusion

Immutability in JavaScript is a set of patterns and tools that help you avoid accidental state mutation and write more robust, testable code. Whether you use const, Object.freeze, property descriptors, deep freeze functions, or Proxy traps, apply the right tool for the problem and keep performance and clarity in mind.

Stay updated with Netalith

Get coding resources, product updates, and special offers directly in your inbox.