Object.isFrozen()
ES5+Returns true if an object is frozen: non-extensible, with every own property non-configurable and every data property non-writable. Ordinary objects return false until Object.freeze() (or equivalent descriptor surgery) has been applied, and primitives passed to it simply return true.
Syntax
Object.isFrozen(obj)Parameters
obj Object The object to check
Return Value
true if the object is frozen, otherwise false
Examples
const obj = { a: 1 };
console.log(Object.isFrozen(obj));
Object.freeze(obj);
console.log(Object.isFrozen(obj)); 📌 When to Use
Use Object.isFrozen() to verify immutability assumptions rather than trusting documentation. Typical call sites: guard clauses in functions that receive objects from unknown callers and plan to mutate them (fail fast with a clear error instead of a silent no-op in sloppy mode), development-time assertions that configuration and shared constants really were frozen at startup, and test suites that assert a store or cache hands out frozen snapshots. It also supports branching logic in defensive utilities — a "safe set" helper can clone-and-modify when given a frozen object but mutate in place when allowed, keeping one API for both styles. When debugging mysterious "my assignment did nothing" reports, Object.isFrozen(obj) is the first question to ask, followed by Object.isSealed() and Object.isExtensible() to pinpoint the exact restriction level on the object. Remember the check is shallow, exactly like freeze itself: a true result says nothing about nested objects, so audits of deep structures need a recursive isDeepFrozen walker like the one shown in the example below.
⚠️ Common Mistakes
Trusting isFrozen as proof that nothing inside can change - a frozen object whose properties point to unfrozen arrays or objects still allows deep mutation: frozen.config.list.push(x) succeeds without complaint. Only a recursive check (or a recursive freeze) covers the whole object graph.
Overlooking that an empty non-extensible object counts as frozen - Object.isFrozen(Object.preventExtensions({})) is true because there are no properties left to be writable or configurable. Code inferring "someone called freeze" from a true result can be misled by this vacuous case.
Assuming Object.isFrozen() checks nested objects - it only checks the top level. Each nested object must be checked separately.
Confusing frozen with sealed - a frozen object is always sealed, but a sealed object is not necessarily frozen.
✅ Best Practices
When a frozen object needs to "change", derive a modified copy instead of trying to unfreeze - unfreezing is impossible by design, and no API reverses Object.freeze(). The idiomatic move is copy-on-write: const next = Object.freeze({ ...frozen, updatedAt: Date.now() }) produces a new frozen snapshot while the old one stays valid for anyone still holding it. This is exactly the discipline state-management libraries formalize, and pairing it with an Object.isFrozen() assertion in tests keeps accidental in-place mutation from creeping back in as the codebase grows.
Fail fast in mutating utilities: if (Object.isFrozen(target)) throw new TypeError("target is frozen") converts sloppy-mode silent failures into actionable errors raised at the call site that actually caused the problem, instead of leaving stale data to be discovered later.
Assert immutability in tests rather than production code - expect(Object.isFrozen(constants)).toBe(true) documents and enforces the contract at CI time with zero runtime cost in the shipped bundle.
Create an isDeepFrozen utility to check all nested objects: function isDeepFrozen(obj) { return Object.isFrozen(obj) && Object.values(obj).filter(v => typeof v === "object").every(isDeepFrozen); }
Use in assertions during development: console.assert(Object.isFrozen(config), "Config should be frozen")
⚡ Performance Notes
Object.isFrozen() must confirm the object is non-extensible and then verify every own property is non-configurable (and non-writable for data properties), so it is technically O(n) over own properties rather than a single flag read — engines short-circuit as soon as any property fails the test, so unfrozen objects usually answer immediately. In practice it is extremely cheap for the small objects it is typically applied to, and perfectly fine in assertions and guard clauses. Avoid calling it inside per-item hot loops over large objects when the answer cannot change between iterations; hoist the check before the loop. A recursive isDeepFrozen audit over a large tree does O(total properties) work and belongs in tests or startup validation, not in per-request production code paths.
🌍 Real World Example
Defensive Configuration Validation
This defensive-initialization example refuses to fully trust its caller: before wiring a config object into the application, it runs isDeepFrozen, a recursive walker built on Object.isFrozen(), and warns when any level of the tree is still mutable. The test sequence demonstrates the shallow-check trap directly — the root reports frozen while the nested settings object stays writable until it is frozen too. The pattern suits library entry points that receive configuration objects from arbitrary user code and need integrity guarantees.
function isDeepFrozen(obj) {
if (!Object.isFrozen(obj)) return false;
return Object.values(obj)
.filter(value => value !== null && typeof value === 'object')
.every(isDeepFrozen);
}
function initializeApp(config) {
// Defensive check - ensure config is immutable
if (!isDeepFrozen(config)) {
console.warn('Warning: Config should be frozen for safety');
// Optionally freeze it ourselves
// config = deepFreeze(config);
}
console.log('App initialized with:', config.appName);
}
// Test cases
const mutableConfig = { appName: 'MyApp', settings: { debug: true } };
console.log(Object.isFrozen(mutableConfig)); // false
Object.freeze(mutableConfig);
console.log(Object.isFrozen(mutableConfig)); // true
console.log(isDeepFrozen(mutableConfig)); // false (nested object not frozen)
Object.freeze(mutableConfig.settings);
console.log(isDeepFrozen(mutableConfig)); // true