All topics
Objectsintermediate

Object.freeze, seal, and Immutability Patterns

Built-in methods for restricting how much an object can be changed, and why true deep immutability requires more than a single call.

JavaScript objects are mutable by default, and Object.freeze() and Object.seal() are the built-in tools for restricting that mutability, though neither goes as deep as many developers initially assume, which makes for a good interview trap question. Immutability patterns matter broadly in state management (Redux, React) where predictable, non-mutated data is central to how change detection and re-rendering work.

Object.freeze is like laminating the cover page of a document — the cover itself can't be altered anymore, but if the cover references a separate folder full of loose pages (a nested object), those loose pages are still completely free to be rewritten unless you laminate every single page inside that folder too.

Key Concepts

1
Object.seal(obj) prevents adding or removing properties from obj — the set of keys is locked — but existing writable properties can still have their values changed. Object.freeze(obj) goes further: it seals the object *and* makes every existing data property non-writable, so property values cannot be reassigned either (attempts silently fail in non-strict mode, or throw a TypeError in strict mode). Both Object.isSealed() and Object.isFrozen() let you check an object's current status.
Object.seal(obj)objObject.freeze(obj)TypeErrorObject.isSealed()
2
The critical gotcha with Object.freeze() is that it's shallow: freezing an object only locks its own top-level properties, but if any of those properties hold a reference to another object or array, that nested object remains fully mutable, since freezing never recurses into it automatically. Object.freeze({ a: { b: 1 } }).a.b = 2 succeeds silently, changing the nested value, even though the outer object is frozen — a very common source of confusion.
Object.freeze()Object.freeze({ a: { b: 1 } }).a.b = 2
3
True deep immutability in real code typically comes from either a recursive freeze utility applied manually, an immutable data library (Immer, Immutable.js) that manages structural sharing efficiently, or simply the discipline of never mutating and always creating new objects/arrays via spread syntax or array methods that return copies. Many teams favor this last convention-based approach over Object.freeze() in practice, since Object.freeze()'s runtime checks add overhead and its shallow nature requires extra tooling to enforce properly at every nesting level anyway.
Object.freeze()