Freezing, sealing & immutability
Lock JavaScript objects with preventExtensions, seal, and freeze; inspect the lock state; write a cycle-safe deepFreeze helper; and separate const from true immutability.
- 01Choose the right lockCompare preventExtensions, seal, and freeze by trying real operations.
- 02Inspect descriptorsUse isExtensible, isSealed, isFrozen, and descriptors to explain what changed.
- 03Avoid shallow surprisesExplain shallow freeze, write deepFreeze, and distinguish const from frozen.
Locking an object
Objects are flexible by default. You can add a property, replace a value, delete a property, or redefine its descriptor flags. That is useful while an object is being built, but it can be risky when the object represents configuration, a public API, or a value other code should not casually reshape.
This lesson is about the built-in object locks. They do not create a new object. They change the object you already have, in bulk, using the property descriptor ideas from Property flags & descriptors: configurable decides whether a property can be deleted or reconfigured, and writable decides whether a data property’s value can be assigned.
Imagine an object as a building. preventExtensions closes the building to new rooms: existing rooms still exist, and some can still be removed. seal also locks the walls: no new rooms and no demolition, but you may still redecorate a room by changing a writable value. freeze turns the building into a museum exhibit: no new rooms, no demolition, and no redecorating.
- In real life: Close the building to new rooms
- In JavaScript:
Object.preventExtensions(obj) - In real life: Lock the walls against demolition
- In JavaScript:
Object.seal(obj) - In real life: Turn the building into a museum exhibit
- In JavaScript:
Object.freeze(obj) - In real life: Freeze every building on campus
- In JavaScript: A recursive
deepFreeze(obj)
Where the analogy stops: Real buildings have doors, owners, and keys. JavaScript locks are descriptor rules on one object. They do not automatically freeze objects stored inside it.
Strict mode matters. In strict code, failed writes throw a TypeError. In sloppy classic scripts, many failed writes silently do nothing. You met strict mode earlier; this lesson proves both outcomes in tests and shows strict results in the playground.
preventExtensions: no new rooms
INTERACTIVEObject.preventExtensions(obj) is the lightest lock. It changes one big fact about the object: no new own properties can be added. Existing properties keep the descriptor flags they already had. If room was writable, you can still assign a new value. If it was configurable, you can still delete it.
That makes preventExtensions useful when the shape of an object must stop growing, but the existing fields are still active. It is like closing construction permits while letting people keep using and remodeling the rooms already inside.
const settings = { theme: "dark", retries: 2, nested: { count: 1 } };// no locksettings.retries = 3;settings.newFlag = true;delete settings.theme;settings.nested.count = 2;Object.defineProperty(settings, "retries", { value: 4 });- Strict outcome
- not run yet
- Sloppy outcome
- added extra
- Object checks
- extensible: true · sealed: false · frozen: false
roomdescriptor- writable: true · configurable: true
Choose a lock level and an operation. The strict result is computed when you press Run; the sloppy result is the same operation proved in tests with classic-script semantics.
Try preventExtensions with each operation. Adding fails. Changing visits works. Deleting room works because ordinary object-literal properties start configurable. Redefining visits works in this example because it is still configurable and writable. The nested write works because the nested object is a separate object.
The same blocked add produces different signals. Strict code throws so bugs are loud. Sloppy scripts often leave the object unchanged and keep going. Modern modules and this Next.js code are strict, but you may still read old snippets or classic scripts.
seal: no additions, no demolition
STEP THROUGHObject.seal(obj) includes non-extensibility, then sets every own property’s configurable flag to false. Once sealed, properties cannot be deleted and their descriptor shape cannot be changed. Data properties that are still writable can still receive new values.
The building analogy says: no new rooms, and no knocking down walls. But if a room is writable, you can still redecorate it. That is why a sealed settings object can allow settings.retries = 3 while blocking delete settings.retries.
Step through the descriptor changes. Seal locks configurability; freeze also locks writability.
script
Object.seal(ticket);const sealed = Object.getOwnPropertyDescriptors(ticket); const badge = { name: "Ada", score: 10 };Object.freeze(badge);const frozen = Object.getOwnPropertyDescriptors(badge);Step through the descriptors. seal changes configurable to false; it does not change writable for data properties. Accessor properties, from the Getters & setters lesson, have no writable flag, so freezing and sealing affect their configurability, not the behavior inside a setter.
freeze: a museum exhibit
SORTObject.freeze(obj) is the strongest built-in lock for an ordinary object. It makes the object non-extensible, makes every own property non-configurable, and for data properties sets writable to false. In strict code, assigning to a frozen data property throws a TypeError.
Freeze is still about properties. If a property contains a reference to another object, the reference cannot be swapped, but the object it points at may still be changed. That shallow behavior is the next section’s big surprise.
- Change an existing writable value after
preventExtensions - Delete an existing configurable property after
preventExtensions - Change an existing writable value after
seal - Change
obj.nested.countafter freezing onlyobj - Delete a sealed own property
- Assign a new value to a frozen data property
- Add a brand-new property after
preventExtensions - Delete a frozen own property
Sort each operation by the lock level where it is still allowed, or mark it blocked. Read every explanation; several cards are intentionally tricky.
A frozen array cannot grow. In strict code, list.push(...) throws because push must add an index and update length. Freezing an array is common for constant lookup lists, but do not treat it as a performance trick.
Checking lock levels
JavaScript gives you three checks that match the locks:
Object.isExtensible(obj)asks whether new own properties may be added.Object.isSealed(obj)asks whether the object is non-extensible and all own properties are non-configurable.Object.isFrozen(obj)asks whether it is sealed and all own data properties are non-writable.
| State | Extensible? | Sealed? | Frozen? | Existing writable values |
|---|---|---|---|---|
| Plain object | true | false | false | Can change |
After preventExtensions | false | Usually false | Usually false | Can change if writable |
After seal | false | true | Maybe false | Can change if writable |
After freeze | false | true | true | Cannot change data properties |
Empty object after preventExtensions | false | true | true | No properties to violate the rules |
An empty object that has only been passed to Object.preventExtensions is also sealed and frozen. There are no own properties left that could be configurable or writable, so the stricter checks pass.
When you need the exact flags, use Object.getOwnPropertyDescriptors(obj). The inspection methods are excellent summaries, but descriptors explain why a write, delete, or definition is allowed.
Deep freeze: every building on the campus
INTERACTIVEObject.freeze is shallow. It freezes the main hall, not every building on campus. If team.lead points to another object, the lead property cannot point elsewhere after a freeze, but the lead object itself may still be mutable.
Predict whether line 6 changes the nested value. Then step through the shallow freeze.
script
name: "UI", lead: { name: "Ada", points: 1 },};Object.freeze(team);team.lead.points = 2;console.log(team.lead.points);console.log(Object.isFrozen(team.lead));A deep freeze walks the object graph and freezes each nested object it reaches. A safe helper needs a WeakSet so cycles do not recurse forever: an object can point back to itself, or two properties can share the same nested object.
function deepFreeze(value, seen = new WeakSet()) { if (value === null || typeof value !== "object") return value; if (seen.has(value)) return value; seen.add(value); for (const key of Reflect.ownKeys(value)) { deepFreeze(value[key], seen); } return Object.freeze(value);}- Nested write after deepFreeze
- not run yet
- Frozen checks
- not run yet
- Frozen array push
- not run yet
- Frozen instance with #private
- not run yet
Read the helper first. It returns primitives, skips objects it has already seen, recursively freezes property values, then freezes the object itself.
Deep freezing is a defensive technique, not the whole story of immutability. Later, the Immutability lesson in Stage 8 focuses on patterns that create updated copies instead of mutating existing objects.
const vs frozen
INTERACTIVEconst and Object.freeze solve different problems. const locks a binding: the name cannot be pointed at a different value. It says nothing about what happens inside the object currently attached to that name.
A const binding is like a label glued to one building. It tells the label not to move to another building. It does not say whether people can repaint the rooms inside. Freezing is what locks the rooms.
- In real life: A label glued to one building
- In JavaScript:
const profile = ...cannot be reassigned - In real life: People repaint rooms inside
- In JavaScript: Properties can still change
- In real life: Museum ropes inside the building
- In JavaScript:
Object.freeze(profile)blocks property changes
Where the analogy stops: A real label can fall off. A JavaScript const binding cannot be reassigned by code in the same scope, but object properties may still be mutable.
| Question | const | Object.freeze |
|---|---|---|
| What is locked? | The binding/name | The object’s own properties |
Can obj.x = 2 work? | Yes, if the property allows it | No for own frozen data properties in strict code |
Can obj = {} work? | No for a const binding | Freeze does not control the variable |
| Is it deep? | Not about object contents | No, shallow by default |
const profile = { name: "Ada", score: 1 };profile.score = 2; // allowed: the object changedObject.freeze(profile);profile.score = 3; // blocked in strict code// profile = {}; // Syntax/runtime error: const label cannot point elsewhere- Before freeze
- not run yet
- After freeze
- not run yet
- Object.isFrozen(profile)
- not run yet
Predict: does const stop line 2? Does freeze stop line 4? Run to separate the binding from the object.
Where you use it
These locks are most useful at boundaries. They make accidental mutation loud while code is being developed, and they document that a value should be treated as finished.
- Freeze constant lookup data, such as a table of keyboard shortcuts or allowed roles.
- Seal objects whose shape is part of an API, but whose values still update, such as a long-lived metrics record.
- Prevent extensions when plugin code should not invent new fields on an object you own.
- Deep freeze test fixtures so one test cannot accidentally mutate a fixture used by the next test.
Do not freeze objects because someone promised a speed boost. Engine optimizations change, and frozen objects are not a reliable performance strategy. Use locks for correctness, clearer APIs, and catching mistakes.
Common misconceptions
“Freeze is deep.”
No. Object.freeze is shallow. Nested objects need to be frozen separately, usually with a recursive helper.
“const freezes objects.”
const locks the binding, not the object. A const object can still be mutated unless its properties prevent it.
“seal makes values read-only.”
Sealed writable data properties can still change. Seal blocks adding, deleting, and reconfiguring properties.
“Failed writes always throw.”
Strict code throws. Sloppy classic scripts often fail silently for assignment and return false for blocked delete.
“Frozen class instances cannot change any state.”
Private fields are not object properties. A method can still update #private state even when Object.isFrozen(instance) is true.
“Freeze is a speed button.”
Use it for correctness. Do not make broad performance claims without measuring in your real program.
Practice: lock objects on purpose
5 EXERCISESTrace the code and type the two console lines in order.
"use strict";
const config = { mode: "dev", retries: 1 };
Object.seal(config);
config.retries = 2;
try { delete config.mode; } catch (error) { console.log(error.name); }
console.log(config.retries, config.mode);The delete throws TypeError, then retries is 2 and mode is still dev, so the two console lines are TypeError and 2 dev.
What does this code print?
"use strict";
const config = { nested: { count: 1 } };
Object.freeze(config);
config.nested.count = 2;
console.log(config.nested.count, Object.isFrozen(config.nested));The nested write succeeds, so count becomes 2. The nested object is not frozen, so the check is false.
A shared config object is being changed by accident. Use deepFreeze when creating it, then verify the nested api object is frozen.
const config = deepFreeze({
api: { retries: 2 },
});
try { config.api.retries = 9; } catch (error) { console.log(error.name); }
console.log(Object.isFrozen(config.api));const config = deepFreeze({
api: { retries: 2 },
});
console.log(Object.isFrozen(config.api));A shallow freeze would still allow config.api.retries to change. deepFreeze freezes both the outer object and config.api, so the nested check is true.
Fill in the blank: const locks the ___, freeze locks the object.
const locks the binding (or label/reference). It prevents profile = other, but it does not freeze profile.score.
You froze a config with deepFreeze. What expression should you use to prove the nested API options are frozen too?
console.log(Object.isFrozen(config));
console.log(Object.isFrozen(config.api));For deep immutability, check both levels you care about. The final nested check should be Object.isFrozen(config.api).
Quiz: check your understanding
7 QUESTIONSThese questions mix definitions with real output. Read the explanation for every choice, especially when you guess correctly.
Question 1 of 7What does
Object.preventExtensions(obj)block?Choose an answer to see the explanation.
Question 2 of 7What does the sealed counter print?
Read the code, then predict"use strict"; const item = { count: 1 }; Object.seal(item); item.count = 2; console.log(item.count);Choose an answer to see the explanation.
Question 3 of 7Which descriptor flags does
Object.freezeset on data properties?Choose an answer to see the explanation.
Question 4 of 7What does the empty-object edge case print?
Read the code, then predictconst empty = {}; Object.preventExtensions(empty); console.log(Object.isSealed(empty), Object.isFrozen(empty));Choose an answer to see the explanation.
Question 5 of 7What does the shallow-freeze nested write print?
Read the code, then predictconst box = { nested: { value: 1 } }; Object.freeze(box); box.nested.value = 2; console.log(box.nested.value);Choose an answer to see the explanation.
Question 6 of 7After
const user = { name: "Ada" }, which statement is true?Choose an answer to see the explanation.
Question 7 of 7Why should a
deepFreezehelper keep aWeakSetof seen objects?Choose an answer to see the explanation.
Key takeaways
Object.preventExtensionsblocks adding new own properties only.Object.sealalso makes own properties non-configurable, so deletion and reconfiguration are blocked.Object.freezealso makes data properties non-writable.- Use
Object.isExtensible,Object.isSealed,Object.isFrozen, and descriptors to inspect the exact state. - Freeze is shallow; a cycle-safe
deepFreezerecursively freezes nested objects. constlocks the binding, not the object’s inside.
Remember the one-liner.
preventExtensions stops new properties; seal stops shape changes; freeze stops shape and writable data changes on that object.
Up next: Enumerability & ownership, where you learn which properties loops and object methods can see.