completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

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.

By the end, you can
  • 01
    Choose the right lockTry adding, changing, and deleting properties to see what preventExtensions, seal, and freeze each block.
  • 02
    Check how locked an object isUse isExtensible, isSealed, isFrozen, and the property flags to explain what each lock changed.
  • 03
    Lock nested objects tooExplain why freeze only locks the outer object, write a deepFreeze helper, and tell const apart from freeze.

Locking an object

Normally, you can change an object in many ways:

  • Add a new property.
  • Change the value of a property.
  • Delete a property.
  • Change a property's flags with Object.defineProperty.

That freedom is useful while you build an object. But some objects should stay as they are once they are ready. Think of the settings of an app, or a list of user roles that a lot of code reads. If some code changes them by mistake, the bug shows up far away from the line that caused it.

JavaScript has three built-in locks for objects: Object.preventExtensions, Object.seal, and Object.freeze. Each one changes the object you give it. None of them makes a copy.

The locks work through the property flags from the last lesson, Property flags & descriptors. Every property has hidden settings called flags. Two of them matter here:

  • writable: can the property's value be changed?
  • configurable: can the property be deleted, or its flags be changed?
Real-life analogyA building with stronger locks

Think of an object as a building, and its properties as rooms. preventExtensions closes the building to new rooms. The rooms already there stay open. You can redecorate them, and you can even knock some down.

seal also stops anyone knocking rooms down. You can still redecorate. freeze turns the building into a museum exhibit: no new rooms, no knocking down, and no redecorating. A deep freeze does this to every building on the campus, not just one.

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 deepFreeze(obj) helper
Three locks, from weakest to strongest. preventExtensions blocks adding. seal blocks adding and deleting. freeze blocks adding, deleting, and changing.preventExtensions()no addingseal()no adding, no deletingfreeze()no adding, no deleting, no changing
Each lock blocks everything the lock above it blocks, plus one more thing. freeze() is the strongest.

What happens when code tries something that a lock blocks? That depends on strict mode. Strict mode is a set of stricter rules. You turn it on by writing "use strict", and it is always on in modules and classes.

  • In strict code, a blocked change throws a TypeError. A TypeError is the error JavaScript throws when you try something a value doesn't allow.
  • In code that is not strict, many blocked changes just do nothing. There is no error, so the mistake is easy to miss.
The same blocked add, without and with strict modePop out in the code editor (opens in a new tab)JavaScript
const cart = { items: 1 };
Object.preventExtensions(cart);

cart.newItem = "tea";
console.log(Object.hasOwn(cart, "newItem"));

function strictAdd() {
  "use strict";
  try {
    cart.otherItem = "coffee";
  } catch (error) {
    console.log(error.name);
  }
}

strictAdd();

Line 2 locks cart, so it can't get new properties. Line 4 tries to add newItem anyway. This line is not in strict mode, so nothing happens and there is no error.

Line 5 asks whether cart now has its own newItem property. Object.hasOwn answers that question, and it prints false.

The function strictAdd turns on strict mode on line 8. So the add on line 10 throws a TypeError. The catch on line 11 catches it, and line 12 prints its name, TypeError.

That is why strict mode helps when you test locked objects. A blocked change shows up as an error instead of hiding.

preventExtensions: no new properties

INTERACTIVE

Object.preventExtensions(obj) is the weakest lock. It does one thing: it stops new own properties from being added. An own property is one stored on the object itself. An object that can't get new properties is called non-extensible.

Properties that already exist keep their flags. So:

  • If a property is writable, you can still change its value.
  • If a property is configurable, you can still delete it.

Use preventExtensions when an object should stop growing, but its current properties should still work as before.

Try each lock
The code you are tryingPop out in the code editor (opens in a new tab)JavaScript
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 });
Resultno lock
In strict mode
not run yet
Without strict mode
added newFlag
The three checks
isExtensible: true · isSealed: false · isFrozen: false
Flags of theme
writable: true · configurable: true
Try it yourself
Lock:Try:

Pick a lock and an action, then press Run. The strict result is worked out when you press Run. The result without strict mode is for the same action, checked by the lesson's tests.

In strict mode, a blocked change throws a TypeError. Without strict mode, a blocked assignment does nothing, and a blocked delete returns false. Object.defineProperty throws in both when it tries to change a frozen value.

Pick preventExtensions and try each action. Here is what happens:

  • add property is blocked. This is the one thing the lock stops.
  • change value works: retries becomes 3.
  • delete property works. Properties written inside { } start out configurable, so theme can be deleted.
  • change nested value works. settings.nested holds a separate object, and the lock only covers settings itself.
  • redefine value works. It changes retries with Object.defineProperty, and retries is still configurable and writable.
Strict and non-strict code

The same blocked add gives a different signal in each kind of code. Strict code throws an error, so you notice the bug. Code that is not strict often leaves the object as it was and keeps going. Modules are always strict, but you may still read older scripts that are not.

seal: no adding, no deleting

STEP THROUGH

Object.seal(obj) does everything preventExtensions does. Then it sets the configurable flag of every own property to false. After that:

  • You can't add new properties.
  • You can't delete properties.
  • You can't change a property's flags.
  • You can still change a value, if the property is writable.

So a sealed settings object still allows settings.retries = 3. But it blocks delete settings.theme.

The settings object after Object.seal. Left: settings.retries = 3 works, so retries is now 3. Right: delete settings.theme is blocked, so theme is still "dark".settings.retries = 3retries: 3settingsworksdelete settings.themetheme: "dark"settingsblocked
After Object.seal(settings), you can still change a value, so retries becomes 3. You can't delete a property, so theme stays.
Watch seal and freeze change the flags
Step 0 of 6Ready
Your turn: follow the blue line

Step through and watch the flags. seal sets configurable to false. freeze also sets writable to false.

Running in
  1. script
Next: line 1
Object.seal(ticket);const sealed = Object.getOwnPropertyDescriptors(ticket); const badge = { name: "Ada", score: 10 };Object.freeze(badge);const frozen = Object.getOwnPropertyDescriptors(badge);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Step through the code. It uses Object.getOwnPropertyDescriptors(obj), which returns the flags of every own property. Watch two flags:

  • seal changes configurable to false.
  • seal leaves writable as it was.

One more detail. An accessor property is a property that runs a getter or a setter function, from the Getters & setters lesson. It has no writable flag. So seal and freeze only stop it from being deleted or redefined. They don't change what its getter or setter does.

freeze: no changes at all

SORT

Object.freeze(obj) is the strongest built-in lock. It does three things:

  • It stops new properties, like preventExtensions.
  • It sets configurable to false on every own property, like seal.
  • It sets writable to false on every data property. A data property is one that holds a value, not a getter or setter.

So no value on the object can change. In strict code, assigning to a frozen property throws a TypeError.

freeze only covers the object's own properties. A property can hold another object. Then the property can't be pointed at a different object, but that other object can still change. You'll see this in the "Deep freeze" section below.

Allowed after freeze or seal?
  • Change a writable value after preventExtensions
  • Delete a configurable property after preventExtensions
  • Change a writable value after seal
  • Change obj.nested.count after freezing only obj
  • Delete a property of a sealed object
  • Give a frozen property a new value
  • Add a brand-new property after preventExtensions
  • Delete a property of a frozen object
Try it yourself
0 of 8 correct

Sort each action into the lock that still allows it. If the lock blocks it, put it in 'Blocked by that lock'. Read the explanation on every card; a few are tricky.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
Arrays are objects too

A frozen array can't grow. In strict code, list.push(...) throws a TypeError, because push has to add a new index and change length. Freezing is a common way to protect a fixed list. It is not a way to make code faster.

Checking lock levels

JavaScript has three methods that tell you how locked an object is. Each one returns true or false:

  • Object.isExtensible(obj): can new properties be added?
  • Object.isSealed(obj): is the object non-extensible, and are all its own properties non-configurable?
  • Object.isFrozen(obj): is the object sealed, and are all its own data properties non-writable too?
Check one object as its lock gets strongerPop out in the code editor (opens in a new tab)JavaScript
const cart = { items: 1 };
console.log(Object.isExtensible(cart), Object.isSealed(cart), Object.isFrozen(cart));
Object.seal(cart);
console.log(Object.isExtensible(cart), Object.isSealed(cart), Object.isFrozen(cart));
Object.freeze(cart);
console.log(Object.isExtensible(cart), Object.isSealed(cart), Object.isFrozen(cart));
  • Line 2 checks a plain object. It prints true false false.
  • Line 3 seals the object. Line 4 prints false true false.
  • Line 5 freezes the object. Line 6 prints false true true.

Each method answers a different question. If you only check isFrozen, you miss the middle state: sealed, but values can still change.

The table below shows what the three checks return after each lock. Read each row as: "after this lock, the checks say this, and existing values can or can't change."

What the three checks return after each lock
After this lockisExtensible?isSealed?isFrozen?Can existing values change?
No lock: a plain objecttruefalsefalseYes.
preventExtensionsfalseUsually false.Usually false.Yes, if the property is writable.
sealfalsetrueUsually false.Yes, if the property is writable.
freezefalsetruetrueNo, not for data properties.
preventExtensions on an empty objectfalsetruetrueThere are no properties to change.
A surprise: the empty object

An empty object passed to Object.preventExtensions also counts as sealed and frozen. Why? Sealed means "no configurable own properties", and frozen means "no writable own properties". An empty object has no properties at all, so both checks pass.

To see the exact flags of each property, use Object.getOwnPropertyDescriptors(obj). The three checks give a quick summary. The flags explain why a change, a delete, or a redefine is allowed or blocked.

Deep freeze: lock the nested objects too

INTERACTIVE

Object.freeze is shallow. Shallow means it locks only the object you give it, not the objects stored inside it.

In the code below, team.lead holds a second object. After Object.freeze(team), the lead property can't point to a different object. But the lead object itself is not frozen, so its points can still change.

Freeze locks only the outer object
Step 0 of 5Ready
Your turn: follow the blue line

Guess first: does line 6 change points? Then step through to check.

Running in
  1. script
Next: line 1
  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));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Left: the team object, holding name "UI", is frozen by Object.freeze(team). Its lead property points to a second object on the right, team.lead. That object is not frozen, so team.lead.points = 2 works and points is now 2.Object.freeze(team)name: "UI"teamfrozenleadteam.lead.points = 2points: 2team.leadnot frozen
Object.freeze(team) locks only the team object. The object that lead points to is a separate object, so it can still change.

A deep freeze freezes the object, then every object inside it, then every object inside those, and so on. The helper below does this. It calls itself for each property value. A function that calls itself is called recursive.

The helper uses two tools you may not have seen yet:

  • Reflect.ownKeys(value) lists every own property name of an object, including symbol keys.
  • A WeakSet is a set that holds objects. The helper adds each object to it, so it can tell which objects it has already visited.

Why remember visited objects? An object can point back to itself, like config.self = config. This is called a cycle. Two properties can also share the same nested object. Without the WeakSet, a cycle would make the helper call itself forever.

Deep freeze, arrays, and private fields
A deepFreeze helper that handles cyclesPop out in the code editor (opens in a new tab)JavaScript
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);}
Checksready
Change the inner object after deepFreeze
not run yet
Is it frozen?
not run yet
push on a frozen array
not run yet
Frozen instance with a #private field
not run yet
Try it yourself

Read the helper first. Line 2 returns simple values like numbers as they are. Line 3 skips objects it has already seen, and line 4 remembers this one. Lines 5 to 7 freeze every property value. Line 8 freezes the object itself.

This helper is kept small on purpose. Helpers in real libraries often have extra options, for example for Maps, Sets, or functions.

A deep freeze protects objects from change. It is not the only way to avoid unwanted changes. Later, the Immutability lesson shows another way: make an updated copy instead of changing the original object.

const vs frozen

INTERACTIVE

const and Object.freeze lock different things. const locks the variable: once the name points to a value, it can't be pointed at a different value. It doesn't lock anything inside that value.

Real-life analogyconst is a label on one building

A const variable is like a label glued to one building. The label can't be moved to another building. But the label says nothing about the rooms inside, so people can still repaint them. Freezing is what stops the repainting.

In real life: A label glued to one building
In JavaScript: const profile = ... can't 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
A name tag, profile, points with an arrow to an object that holds score 2. const keeps the arrow fixed, so profile always points to this object. Object.freeze(profile) locks what is inside the object.const profileprofileconst keepsthis arrowObject.freeze(profile)score: 2objectfreeze lockswhat is inside
const and Object.freeze lock two different things. const keeps the name pointing at the same object. freeze keeps the object's properties from changing.

The table compares the two. Read each row as a question, then the answer for const and for Object.freeze.

What const locks and what Object.freeze locks
QuestionconstObject.freeze
What does it lock?The variable: the name always points to the same value.The object's own properties.
Can obj.x = 2 work?Yes, if the property allows it.No. In strict code it throws a TypeError.
Can obj = {} work?No. A const variable can't be given a new value.Freeze doesn't decide this. It doesn't control the variable.
Does it lock nested objects?No. It doesn't lock anything inside the object.No. It is shallow, unless you use a deep freeze.
const keeps the name, freeze locks the object
const vs Object.freezePop out in the code editor (opens in a new tab)JavaScript
const profile = { name: "Ada", score: 1 };profile.score = 2;      // allowed: the object changedObject.freeze(profile);profile.score = 3;      // blocked in strict code// profile = {};        // TypeError: a const variable can't get a new value
Outputready
Before freeze
not run yet
After freeze
not run yet
Object.isFrozen(profile)
not run yet
Try it yourself

Guess first: does const stop line 2? Does freeze stop line 4? Then run the code to see which one locks the variable and which one locks the object.

Line 5 is a comment, so it doesn't run; running it would stop the example with an error. It reminds you that const stops the name from getting a new value.

Where you use it

Locks are most useful where your code hands an object to other code. They turn an accidental change into an error you can see while you build. They also tell other developers that the value is finished and should not change.

  • Freeze fixed lookup data, such as a table of keyboard shortcuts or a list of allowed roles.
  • Seal an object whose list of properties must stay the same, but whose values still update. One example is an object that counts page visits.
  • Use preventExtensions when plugin code should not add new properties to an object you own. Plugin code is code that other people write to add features to your app.
  • Deep freeze test fixtures. A test fixture is sample data that several tests share. Freezing it means one test can't change the data that the next test uses.
Freeze a settings object and a fixed listPop out in the code editor (opens in a new tab)JavaScript
const appConfig = Object.freeze({ apiUrl: "/api", retries: 2 });
const roles = Object.freeze(["reader", "editor"]);
console.log(Object.isFrozen(appConfig), appConfig.retries);
try {
  roles.push("admin");
} catch (error) {
  console.log(error.name);
}
  • Line 1 freezes appConfig. Line 2 freezes the roles array.
  • Line 3 prints true 2. The object is frozen, but you can still read its values.
  • Line 5 tries roles.push("admin"). push needs to add a new index and change length, so it throws. Line 7 prints TypeError.

Freeze values like these only when they really are fixed. Don't freeze them when other code needs to update them.

Freezing stops changes, but it doesn't give you a way to make an updated version of an object. The Immutability lesson, linked above, shows how to do that with copies.

Freezing is not for speed

Don't freeze objects to make code faster. JavaScript engines change over time, and freezing is not a reliable way to gain speed. Use locks to catch mistakes and to show which objects must not change.

Common misconceptions

“Freeze is deep.”

No. Object.freeze is shallow. It locks only the object you give it. Objects stored inside it must be frozen too, usually with a deepFreeze helper that calls itself.

“const freezes objects.”

No. const locks the variable, not the object. You can still change the properties of a const object, unless a lock stops you.

“seal makes values read-only.”

No. On a sealed object, writable properties can still get new values. seal blocks adding, deleting, and changing flags.

“A blocked change always throws.”

Only in strict code. In code that is not strict, a blocked assignment does nothing and gives no error. A blocked delete returns false.

“A frozen class instance can't change at all.”

Private fields, the ones whose names start with #, are not properties of the object. So freeze doesn't lock them. A method can still update a #private field even when Object.isFrozen(instance) is true.

“Freeze makes code faster.”

Use freeze to catch mistakes, not for speed. Don't say it is faster unless you have measured it in your own program.

Practice: lock objects on purpose

5 EXERCISES
Exercise 1 · Warm-upPredict a sealed object

Trace the code. Type the two lines it prints, in order.

Starter codePop out in the code editor (opens in a new tab)JavaScript
"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);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticePredict a shallow freeze

    What does this code print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    "use strict";
    const config = { nested: { count: 1 } };
    Object.freeze(config);
    config.nested.count = 2;
    console.log(config.nested.count, Object.isFrozen(config.nested));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeStop a config object from changing by mistake

      Some code changes a shared config object by mistake. Create the object with deepFreeze. Then check that the nested api object is frozen too.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const config = deepFreeze({
        api: { retries: 2 },
      });
      try { config.api.retries = 9; } catch (error) { console.log(error.name); }
      console.log(Object.isFrozen(config.api));
        Exercise 4 · PracticeExplain const vs freeze

        Fill in the blank: const locks the ___, freeze locks the object.

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeCheck the nested object with Object.isFrozen

          You froze a config with deepFreeze. Which expression proves that the nested api object is frozen too?

          Answer, then press Check. Spacing and letter case don’t matter.

            Quiz: check your understanding

            7 QUESTIONS

            These questions mix ideas with real code. Read the explanation for every choice, even when you get it right.

            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What does Object.preventExtensions(obj) block?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does the sealed counter print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              "use strict";
              const item = { count: 1 };
              Object.seal(item);
              item.count = 2;
              console.log(item.count);

              Choose an answer to see the explanation.

            3. Question 3 of 7Which flags does Object.freeze set on a property that holds a value?

              Choose an answer to see the explanation.

            4. Question 4 of 7What does the empty-object example print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const empty = {};
              Object.preventExtensions(empty);
              console.log(Object.isSealed(empty), Object.isFrozen(empty));

              Choose an answer to see the explanation.

            5. Question 5 of 7What does this code print after freezing only the outer object?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const box = { nested: { value: 1 } };
              Object.freeze(box);
              box.nested.value = 2;
              console.log(box.nested.value);

              Choose an answer to see the explanation.

            6. Question 6 of 7After const user = { name: "Ada" }, which statement is true?

              Choose an answer to see the explanation.

            7. Question 7 of 7Why should a deepFreeze helper keep a WeakSet of objects it has seen?

              Choose an answer to see the explanation.

            Key takeaways

            • Object.preventExtensions only stops new properties from being added.
            • Object.seal also stops properties from being deleted or having their flags changed. Values can still change.
            • Object.freeze also stops the values of data properties from changing.
            • Object.isExtensible, Object.isSealed, and Object.isFrozen tell you how locked an object is. Object.getOwnPropertyDescriptors shows the exact flags.
            • Freeze is shallow. A deepFreeze helper with a WeakSet freezes the nested objects too, even when an object points back to itself.
            • const locks the variable, not what is inside the object.

            Remember the one-liner.
            preventExtensions stops adding; seal also stops deleting; freeze also stops changing values on that object.

            Up next: Enumerability & ownership. You'll learn which properties loops and object methods can see.

            CompleteFrontend Clear concepts. Working examples.