Object.freeze()

Genuine immutability, with two large caveats: it reaches only one level down, and outside strict mode a blocked write fails without saying anything.

Object static methodES5 (2009)Live demo
Common call
Object.freeze(config)
Returns
the same object, frozen
Replaces
a convention that nobody will mutate it
Watch out
SHALLOW — nested objects stay fully mutable
Object.freeze(objectobject — The object to freeze. Primitives are returned unchanged and count as already frozen, so freezing one is a silent no-op rather than an error.type: any · required)
→ object

Demo

Live evaluation
Try:
Inputs
jsonstringJSON with an "n" property
Output
(o => [Object.isFrozen(o), Object.isFrozen(o.n)])(Object.freeze(JSON.parse('{"a":1,"n":{"x":1}}')))
[true, false]

The output is a pair: whether the object itself is frozen, and whether its n property is. The first case is the whole lesson — the outer object reports true while the nested object reports FALSE. Freezing protects only the top level, so config.n.x = 99 still succeeds on a frozen config. The second and third cases return true for n because primitives are immutable by definition and count as frozen; that makes the predicate look reassuring on flat objects and useless on nested ones. The array case behaves like the object case: arrays are objects, and they are left mutable.

Parameters

NameTypeRequiredDescription
objectanyyesThe object to freeze. Primitives are returned unchanged and count as already frozen, so freezing one is a silent no-op rather than an error.

Return value

object — The SAME object, now frozen — not a frozen copy. Properties cannot be added, removed, reordered or changed.

Common patterns

Freeze a constant
const stops reassignment, not mutation.
export const COLORS = Object.freeze({red: "#f00"});
Freeze deeply
Recurse yourself; there is no built-in.
const deepFreeze = o => {
  Object.values(o).forEach(v => {
    if (v && typeof v === "object") deepFreeze(v);
  });
  return Object.freeze(o);
};
Prefer copying to freezing
Immutable UPDATES rather than enforced immutability.
const next = {...state, count: state.count + 1};

Examples

1. Write blocked (strict)
"use strict"; const o = Object.freeze({a: 1}); o.a = 2
Returns
TypeError: Cannot assign to read only property 'a'
2. Write ignored (sloppy)
const o = Object.freeze({a: 1}); o.a = 2; o.a
Returns
1 // no error at all
3. Shallow
const o = Object.freeze({n: {x: 1}}); o.n.x = 9; o.n.x
Returns
9
4. Same object back
const o = {a: 1}; Object.freeze(o) === o
Returns
true
5. Arrays too
"use strict"; Object.freeze([1]).push(2)
Returns
TypeError: Cannot add property 1, object is not extensible
6. Primitives are frozen
Object.isFrozen(42)
Returns
true

Pitfalls

1. It is shallow
Only the object own properties become read-only. Every nested object and array stays fully mutable, so a frozen configuration object gives no protection at all to the interesting parts of a nested config.
Nested still writable
const c = Object.freeze({db: {host: "x"}});
c.db.host = "y";
c.db.host
'y'
Freeze recursively
deepFreeze(c);
TypeError on write
2. It fails silently outside strict mode
A blocked assignment in sloppy mode does nothing and reports nothing — the value simply stays as it was. ES modules and class bodies are strict automatically, so this mostly bites in plain scripts and in the console.
Silent no-op
const o = Object.freeze({a: 1});
o.a = 2;
o.a
1
Strict throws
"use strict";
const o = Object.freeze({a: 1});
o.a = 2
TypeError
3. const and freeze solve different problems
const prevents REBINDING the variable; freeze prevents MUTATING the value. A const object can have its properties changed freely, and a frozen object can still be replaced if it was declared with let.
const is not immutable
const o = {a: 1};
o.a = 2;
o.a
2
Freeze the value
const o = Object.freeze({a: 1});
mutation blocked
4. Freezing a primitive is a silent no-op
Passing a number or a string returns it unchanged and isFrozen reports true, so a defensive freeze on a value that turned out not to be an object gives false confidence rather than an error.
Looks protected
Object.isFrozen(Object.freeze(42))
true
Check the type first
typeof v === "object" && v !== null
explicit

When to use

Use it
  • Exported constants and lookup tables
  • Catching accidental mutation during development
  • Enforcing that a shared object is not modified by consumers
Reach for something else
  • Nested data → freeze recursively, or do not rely on it
  • You want immutable UPDATES → spread into a new object
  • Hot paths — some engines deoptimise frozen objects in polymorphic code
  • You only want to block adding properties → seal or preventExtensions

Notes

Complexity
O(n) in the number of own properties
Return
The same object reference, now non-extensible with every property non-writable and non-configurable
CPython impl
V8: Builtins-object-freeze
Memory
No allocation
Thread-safe
Single-threaded; freezing is permanent and cannot be undone

FAQ

Recurse over the values yourself — there is no built-in. Watch for cycles, which will otherwise recurse forever, and remember that freezing is irreversible.

const deepFreeze = o => {
  Object.values(o).forEach(v => {
    if (v && typeof v === "object" && !Object.isFrozen(v)) deepFreeze(v);
  });
  return Object.freeze(o);
};

History

ES5
freeze, seal, preventExtensions and their predicates added together.
ES2015
Strict mode became the default inside modules and classes, making blocked writes throw far more often.