Object.freeze(), Object.seal(), and preventExtensions()

Hero image for Object.freeze(), Object.seal(), and preventExtensions(). Image by Alexander Hafemann.
Hero image for 'Object.freeze(), Object.seal(), and preventExtensions().' Image by Alexander Hafemann.

JavaScript objects are very flexible by default. You can add properties, remove properties, and change property values freely. That is convenient most of the time, but there are situations where you want tighter control.

That is where Object.freeze(), Object.seal(), and Object.preventExtensions() come in.

The trouble is that they sound similar enough to blur together when you first meet them. All three restrict an object in some way, but they do not impose the same rules.

Once you understand the differences, they become much less mysterious.


preventExtensions() is the lightest restriction

Object.preventExtensions() stops new properties being added to an object.

const user = {  name: 'Sophie',};Object.preventExtensions(user);

After that, you cannot add a new property such as role.

user.role = 'Editor';

That will fail or be ignored depending on mode and context.

But existing properties can still be changed, and in many cases removable properties can still be deleted.

So preventExtensions() is really saying:

"this object should not grow any further".


seal() goes a step further

Object.seal() also prevents new properties being added, but it goes further by stopping existing properties from being deleted.

const user = {  name: 'Sophie',};Object.seal(user);

Now:

  • you cannot add new properties
  • you cannot remove existing properties

But you can still change the value of an existing writable property.

user.name = 'Ellie';

That still works.

So seal() means the shape of the object is locked, but its existing values may still change.


freeze() is the strongest of the three

Object.freeze() applies the strongest toplevel restriction.

const user = {  name: 'Sophie',};Object.freeze(user);

Now:

  • you cannot add properties
  • you cannot delete properties
  • you cannot change existing property values

At least, not at the top level.

That is why freeze() is usually the one people mean when they talk about making an object immutable. But there is an important caveat to that word.


All Three are Shallow

This is the part beginners most often miss.

These methods only lock the object itself at the level they are applied. They do not automatically freeze or seal nested objects.

For example:

const settings = {  theme: 'dark',  preferences: {    compact: false,  },};Object.freeze(settings);

You cannot replace settings.theme, but the nested object can still be mutated:

settings.preferences.compact = true;

That still works because preferences points to another object that has not itself been frozen.

This is why "frozen" does not necessarily mean "deeply immutable". It usually means "toplevel properties on this object are locked".


Why This Matters in Practice

These methods are useful when you want to:

  • protect configuration objects from accidental shape changes
  • prevent helper objects from gaining ad hoc properties
  • communicate that a value should be treated as fixed
  • make accidental mutation more obvious during development

They are less useful if you imagine they will automatically solve all state mutation problems in a larger nested object graph. They will not.

That means the real value is clarity and guard rails, not some magical universal immutability switch.


A Side‑By‑Side Comparison

Imagine this object:

const product = {  id: 42,  title: 'Notebook',};

With preventExtensions():

  • product.price = 10 is blocked
  • product.title = 'Large Notebook' still works
  • deleting an existing property may still be allowed

With seal():

  • adding new properties is blocked
  • deleting existing properties is blocked
  • changing existing property values still works

With freeze():

  • adding new properties is blocked
  • deleting existing properties is blocked
  • changing existing property values is blocked

That progression is the easiest way to remember them: prevent growth, then lock shape, then lock toplevel values.


They Do Not Replace Disciplined Object Design

It is tempting to think these methods are the answer to all mutation risks. They are not.

If a codebase is hard to reason about because state is shared unpredictably, freezing a few objects does not automatically fix the deeper design issue. It may help catch some mistakes, but you still need good update patterns and clear ownership of data.

That is why these methods are best thought of as constraints, not architecture.


When freeze() is especially nice

freeze() is often helpful for objects that represent fixed definitions or options rather than living state.

For example:

const ROUTES = Object.freeze({  home: '/',  articles: '/articles',  contact: '/contact',});

That communicates intent quite nicely. This object is meant to be read, not reauthored halfway through the application.

That kind of usage is often clearer than applying freeze() reactively to ordinary changing state and expecting it to clean up the surrounding logic.


Checking the State of an Object

JavaScript also gives you helpers to inspect whether an object has been frozen, sealed, or made nonextensible:

  • Object.isFrozen()
  • Object.isSealed()
  • Object.isExtensible()

Those are useful mostly for debugging, testing, or confirming assumptions in utilities.


Wrapping up

Object.preventExtensions(), Object.seal(), and Object.freeze() all restrict objects, but they do so at different levels. preventExtensions() stops growth, seal() locks the object's shape, and freeze() also blocks toplevel value changes. The most important practical caveat is that all three are shallow, so nested objects can still be mutable unless you handle them separately.

Key Takeaways

  • preventExtensions() blocks new properties from being added.
  • seal() blocks additions and deletions but still allows changes to existing writable properties.
  • freeze() blocks additions, deletions, and toplevel value changes.
  • All three are shallow rather than deep.
  • These methods are useful guard rails, but they do not replace sensible state design.

Once you understand the progression from "cannot grow" to "cannot change", these APIs are much easier to choose between deliberately.


Categories:

  1. Development
  2. Front‑End Development
  3. Guides
  4. JavaScript