•8 min read

const vs Immutability in JavaScript: The Binding, Not the Value

const vs Immutability in JavaScript: The Binding, Not the Value
Audio Briefing
0:00 / 0:00
Common Misconception

Many tutorials and even experienced developers oversimplify this. The distinction between reassignment and mutation is the key to understanding JavaScript behavior at a deeper level—getting it wrong leads to subtle bugs in state management.

const in JavaScript does not freeze your values. It locks the variable binding—meaning you can't point the variable at an entirely different value. This is one of the most misunderstood concepts in JavaScript.


The Binding vs. The Value

Reassignment (Blocked)

The binding itself is constant. You cannot point x somewhere else:

const x = 5;
x = 10;  // TypeError: Assignment to constant variable.

const user = { name: "Ava" };
user = {};  // TypeError: Assignment to constant variable.

Mutation (Allowed)

You didn't change the binding—you changed the object/value it points to. JavaScript allows this:

const user = { name: "Ava" };
user.name = "Morgan";  // ✅ Allowed — same object, different property

const items = [1, 2, 3];
items.push(4);         // ✅ Allowed — same array reference
items[0] = 99;         // ✅ Allowed — same array reference

The Memory Model

Think of const as a "permanent pointer" rather than an "immutable object":

const obj = { a: 1, nested: { b: 2 } };

// The pointer (variable name) is locked to this object
// But the object itself is still open for business

obj.a = 100;           // ✅ Fine
obj.nested.b = 200;    // ✅ Fine
obj.newProp = "new";   // ✅ Fine

// Only the final line fails — reassigning the variable itself
obj = {};              // ❌ TypeError

Advertisement

Why JavaScript Designed It This Way

JavaScript uses a const keyword that signals intent rather than enforcing immutability. This choice relates to JavaScript's history as a lightweight scripting language, but also has advantages in everyday development.

  1. Performance: The JavaScript engine knows that the binding won't change, enabling certain optimizations.

  2. Intent signaling: const tells other developers: "This variable should not be reassigned."

  3. Flexibility: Some objects need mutation (adding properties, pushing array elements) but shouldn't be replaced.

  4. Backward compatibility: JavaScript was designed with a minimalist approach, and adding true deep immutability from the start wasn't feasible.


Real Immutability Options

Quick Reference

If you truly need immutability, JavaScript provides several tools—each with different levels of depth and performance implications.

const shallow = Object.freeze({
name: "Ava",
settings: {
    notifications: true
}
});

shallow.name = "Morgan";               // ❌ TypeError
shallow.newProp = "test";              // ❌ TypeError

// ❗ Watch out: nested objects are NOT frozen
shallow.settings.notifications = false; // ✅ Works! Shallow only
shallow.settings = { notifications: false }; // ❌ TypeError (top-level only)

// Verify:
Object.isFrozen(shallow);             // true
Object.isFrozen(shallow.settings);    // false — still mutable!
const original = {
name: "Ava",
settings: {
    notifications: true,
    theme: "dark"
},
tags: ["dev", "tech"]
};

// Creates a true deep copy (no shared references)
const copy = structuredClone(original);

// ✅ Mutating the copy has zero effect on original
copy.name = "Morgan";
copy.settings.notifications = false;
copy.tags.push("blog");

console.log(original.name);             // "Ava"
console.log(original.settings.notifications); // true
console.log(original.tags);             // ["dev", "tech"]

console.log(copy.name);                 // "Morgan"
console.log(copy.tags);                 // ["dev", "tech", "blog"]
Why structuredClone?

It handles Date, RegExp, Map, Set, ArrayBuffers, and even circular references. It's the modern replacement for JSON.parse(JSON.stringify()), preserving types that JSON loses.

MethodCan add props?Can delete props?Can change values?Can change descriptors?
Object.preventExtensions()❌✅✅❌
Object.seal()❌❌✅❌
Object.freeze()❌❌❌❌
Practical advice

These methods are mostly useful for defensive coding or when you want to explicitly lock down an API contract. In application logic, immutable patterns (spreading, structuredClone) are usually clearer and more flexible.

You want immutable-looking code that's actually mutable under the hood

Immer takes a unique approach: you write code that looks mutable, and it produces a new immutable state automatically.

import { produce } from "immer";

const nextState = produce(currentState, (draft) => {
// "mutate" the draft — feels natural
draft.items.push({ id: 3, name: "Pencil" });
draft.items[0].quantity = 5;
});

// nextState is a brand new object
// currentState is unchanged
// But the code reads like you're modifying it

This pattern powers Redux Toolkit's createSlice and many modern state management systems.


Decision Diagram


Advertisement

Best Practices

const Best Practices in 2026
✅ Do❌ Don'tReason
Use const as the defaultUse let as the defaultSignals intent; prevents accidental reassignment
Use structuredClone() for deep copiesUse JSON.parse(JSON.stringify())Loses Dates, undefined, circular refs
Use spread for shallow copies: {...obj}Mutate objects and arrays directlyReact state needs new references to re-render
Use Immer for complex nested updatesManually deep-freeze every nested objectO(n²) complexity and fragile
Use TypeScript readonly for compile-time checksAssume const grants protectionRuntime JS offers zero enforcement for mutation

Common Questions

const prevents accidental reassignment—the most common bug. It tells humans and really smart linters that this binding should stay the same. For true read-only guarantees, combine it with other patterns.

Yes. And you should! In loops, const item creates a new binding per iteration, so it's safe and actually preferred:

const items = [1, 2, 3];

items.forEach((item) => {        // ✅ item is per-iteration
    console.log(item);
});

items.map((item) => item * 2);   // ✅ item is per-iteration
const funcs = [];

for (const i = 0; i < 5; i++) {
    funcs.push(() => console.log(i));
}
funcs[0]();  // 0 — all log their specific i value ✓

// But this works DIFFERENTLY:
for (let j = 0; j < 5; j++) {
    funcs.push(() => console.log(j));
}
funcs[0]();  // 0 — same result here

The distinction only matters in var-style function-scoped hoisting scenarios.


TL;DR

One-sentence takeaway

const means "this variable won't be reassigned"—not "this value is immutable." Use it every day. For immutability, pair it with spread syntax, structuredClone, or a library like Immer.

You Might Also Like

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement