Freezing and Immutability
Mental model: Freezing locks one object, not the objects it points at.
Level: intermediate · about 10 minutes
const settings = Object.freeze({ theme: 'dark', retries: 3 });
try {
settings.theme = 'light'; // strict mode here, so this throws
} catch (err) {
console.log(err.name); // 'TypeError'
}
console.log(settings.theme); // 'dark'
console.log(Object.isFrozen(settings)); // trueA frozen object rejects every change to its own properties.
Object.freeze makes every own property non-writable and non-configurable, and prevents new properties being added. It is the strongest of the three built in locks and the only one that stops values changing.
| Call | Add new keys | Delete keys | Change existing values |
|---|---|---|---|
Object.preventExtensions(o) | no | yes | yes |
Object.seal(o) | no | no | yes |
Object.freeze(o) | no | no | no |
const sealed = Object.seal({ count: 0 });
sealed.count = 5; // allowed
console.log(sealed.count); // 5
console.log(Object.isSealed(sealed), Object.isFrozen(sealed)); // true falseSealed: the shape is fixed, the values are not.
Freezing is shallow too
const config = Object.freeze({ db: { host: 'localhost' } });
config.db.host = 'evil.example'; // no error, no protection
console.log(config.db.host); // 'evil.example'
console.log(Object.isFrozen(config), Object.isFrozen(config.db)); // true false
Deep freeze, in five lines
function deepFreeze(value) {
if (value === null || typeof value !== 'object' || Object.isFrozen(value)) return value;
Object.freeze(value); // freeze first, so cycles terminate
for (const key of Object.keys(value)) deepFreeze(value[key]);
return value;
}
const locked = deepFreeze({ db: { host: 'localhost', ports: [5432] } });
console.log(Object.isFrozen(locked.db), Object.isFrozen(locked.db.ports)); // true true
Why the same write is silent in one file and throws in another
Sloppy mode: silent
// a classic <script> with no 'use strict'
const o = Object.freeze({ a: 1 });
o.a = 2; // ignored, no error
console.log(o.a); // 1
Strict mode and modules: throws
// any ES module, or after 'use strict'
const o = Object.freeze({ a: 1 });
o.a = 2; // TypeError
ES modules and class bodies are strict automatically, so in modern code a rejected write throws. The silent version is the legacy behaviour, and it is the reason people believe freeze "did not work".
Immutable data discipline
- Freeze the things that are meant to be constant: configuration, lookup tables, default values.
- Treat everything else as immutable by convention: return new objects from updates rather than mutating arguments.
- Prefer
map,filterand spread overpush,sortand property assignment in shared data. - Freeze in development and skip it in production if the cost matters. Freezing large hot objects is not free.
const state = Object.freeze({ count: 0, tags: Object.freeze(['a']) });
const next = Object.freeze({ ...state, count: state.count + 1, tags: [...state.tags, 'b'] });
console.log(state.count, next.count); // 0 1
console.log(state.tags, next.tags); // ['a'] ['a', 'b']The update pattern that needs no freezing at all.
const o = Object.freeze({ nested: { n: 1 }, top: 1 });
o.nested.n = 2;
console.log(o.nested.n, Object.isFrozen(o.nested));Freeze applies to the properties of o only. The nested object is a separate object that was never frozen, so writing to it succeeds and Object.isFrozen(o.nested) is false.
Try it yourself
Three locks, side by side
const extendable = { a: 1 };
const sealed = Object.seal({ a: 1 });
const frozen = Object.freeze({ a: 1, nested: { b: 2 } });
extendable.b = 2;
sealed.a = 99;
frozen.nested.b = 99; // nested objects are not protected
console.log(extendable, sealed, frozen);
console.log(Object.isExtensible(extendable), Object.isSealed(sealed), Object.isFrozen(frozen));
Try deleting a key from each object. Then freeze the nested array and watch push throw.
Exercises
Write deepFreeze
Write deepFreeze(value) which freezes an object and everything reachable from its own enumerable properties, including arrays, and returns the same value. Primitives pass straight through. It must not loop forever on a circular structure.
Check yourself
- What does this log?
- 2 — Sealing fixes the shape, not the values, so the assignment succeeds and
abecomes 2. Thedeleteis rejected: in strict mode it throws, in sloppy mode it returnsfalse. Either way the value 2 remains readable. - Why does
Object.freezesometimes appear to do nothing? - In sloppy mode a rejected write fails silently, and freezing is shallow so nested objects stay writable — Both reasons bite in practice. Modules are strict so writes throw there, but nested objects are never frozen for you. Use a deep freeze if you need the whole tree locked.
- You want a configuration object nobody can modify, and its values must stay private. Is freeze enough?
- No, freeze prevents writes but reads are still open. Use a closure or private fields for privacy — Freezing is about mutation, not visibility. Anything on a frozen object can still be read, iterated and serialised. Privacy comes from not exposing the value in the first place.
Common mistakes
- Assuming
Object.freezeis deep. It locks one level only. - Testing freeze in sloppy mode, seeing no error, and concluding it does not work.
- Confusing freeze with privacy. Reads are unaffected.
Takeaways
preventExtensionsstops additions,sealalso stops deletions,freezealso stops writes.- All three are shallow. Recurse yourself if you need the whole tree.
- A rejected write throws in strict mode and modules, and is silent in sloppy mode.
- Immutability is mostly a discipline: return new objects instead of editing shared ones.