Freeze, Seal, and preventExtensions on Objects
JavaScript ships three integrity levels for objects (`Object.preventExtensions`, `Object.seal`, and `Object.freeze`) and they are easy to confuse because each silently relaxes one rule from the next. This snippet builds them up from least to most restrictive, shows the strict-mode behavior that turns silent failures into errors, and finishes with a recursive `deepFreeze` for nested config. Reach for these when you want a runtime guarantee that downstream code cannot mutate a shared object.
840 views
12
const a = Object.preventExtensions({ name: 'Ada', age: 36 });
a.name = 'Grace'; // allowed: existing property is writable
a.role = 'admin'; // ignored: cannot add new properties
delete a.age; // allowed: existing property can be deleted
console.log(a); // { name: 'Grace' }
const b = Object.seal({ name: 'Ada', age: 36 });
b.name = 'Grace'; // allowed: existing property is writable
b.role = 'admin'; // ignored: cannot add
delete b.age; // ignored: cannot delete
console.log(b); // { name: 'Grace', age: 36 }
const c = Object.freeze({ name: 'Ada', age: 36 });
c.name = 'Grace'; // ignored: writes blocked
c.role = 'admin'; // ignored
delete c.age; // ignored
console.log(c); // { name: 'Ada', age: 36 }
console.log(Object.isExtensible(a), Object.isSealed(b), Object.isFrozen(c));
// false true trueThe three calls form a strict ladder of restrictions. preventExtensions blocks new properties only, so existing fields can still be reassigned or deleted. seal adds a no-delete rule on top, but writable values can still change. freeze adds a no-write rule, which is the version most engineers actually want when they say immutable. The matching predicates Object.isExtensible, Object.isSealed, and Object.isFrozen are how you verify the level after the fact, which is handy when an object passes through several layers.
'use strict';
const frozen = Object.freeze({ name: 'Ada' });
try {
frozen.name = 'Grace';
} catch (err) {
console.log(err.name + ':', err.message);
// TypeError: Cannot assign to read only property 'name' of object '#<Object>'
}
try {
frozen.role = 'admin';
} catch (err) {
console.log(err.name + ':', err.message);
// TypeError: Cannot add property role, object is not extensible
}
try {
delete frozen.name;
} catch (err) {
console.log(err.name + ':', err.message);
// TypeError: Cannot delete property 'name' of #<Object>
}Outside strict mode the writes in accordion 1 fail silently, which is the worst possible default for a debugging session. Modules and class bodies are strict by default, but loose-mode scripts and snippet sandboxes are not, so bugs hide in production and surface in testing. Adding 'use strict' (or just authoring inside a module) flips those silent ignores into real TypeError throws, which is what you almost always want during development. The three messages above pin down exactly which rule was violated, so a stack trace tells you whether to relax the integrity level or to fix the call site.
function deepFreeze(obj) {
if (obj === null || typeof obj !== 'object' || Object.isFrozen(obj)) return obj;
for (const value of Object.values(obj)) {
deepFreeze(value);
}
return Object.freeze(obj);
}
const config = deepFreeze({
api: { baseUrl: 'https://api.example.com', timeoutMs: 5000 },
flags: ['beta', 'analytics']
});
try { config.api.timeoutMs = 1; } catch {} // throws in strict mode, ignored otherwise
try { config.flags.push('debug'); } catch {} // throws in strict mode, ignored otherwise
console.log(config.api.timeoutMs); // 5000
console.log(config.flags); // [ 'beta', 'analytics' ]
console.log(Object.isFrozen(config), Object.isFrozen(config.api), Object.isFrozen(config.flags));
// true true trueObject.freeze is shallow: nested objects and arrays are still mutable references, which is a common surprise the first time someone tries to lock down a config tree. The recursive walker freezes leaves before parents and short-circuits on already-frozen objects, which keeps the cost linear and prevents stack overflows on shared subtrees. The Object.isFrozen early exit also handles cycles safely (the second visit hits an already-frozen node and returns). Use this for module-level config and dependency-injection containers that should never be mutated at runtime; for hot per-request data, prefer copy-on-write helpers because freezing makes every write a throw.
