Object.freeze() vs Object.seal() in JavaScript
Object.freeze() locks an object completely; Object.seal() only stops adding or removing properties. How the two immutability methods differ.
Object.freeze() and Object.seal() are two built-in JavaScript methods that restrict how an object can be changed after it’s created, but they stop at different points: freeze() locks an object completely — no new properties, no deleted properties, no changed values — while seal() only prevents adding or removing properties, leaving existing property values free to be reassigned.
What each one actually locks
Both methods work on the object’s own properties and don’t touch anything about its prototype chain. The difference is in how far each one restricts mutation:
Object.seal(obj)
- Prevents adding new properties.
- Prevents deleting existing properties.
- Allows reassigning the values of existing writable properties.
Object.freeze(obj)
- Prevents adding new properties.
- Prevents deleting existing properties.
- Prevents reassigning the values of existing properties too.
In other words, every sealed object has one thing frozen objects don’t: you can still write sealedObj.count = 5 and have it take effect. A frozen object rejects that write silently in non-strict mode, or throws a TypeError in strict mode and inside ES modules (which are strict by default).
const sealed = Object.seal({ count: 0 });
sealed.count = 5; // works — count is now 5
sealed.extra = "no"; // silently ignored — sealed objects reject new props
delete sealed.count; // silently ignored
const frozen = Object.freeze({ count: 0 });
frozen.count = 5; // silently ignored (throws in strict mode)
frozen.extra = "no"; // silently ignored
delete frozen.count; // silently ignored
Comparison
| Mutable object | Object.seal() | Object.freeze() | |
|---|---|---|---|
| Add new property | Yes | No | No |
| Delete property | Yes | No | No |
| Reassign existing value | Yes | Yes | No |
| Reconfigure property descriptor | Yes | No | No |
| Checked with | — | Object.isSealed() | Object.isFrozen() |
Every frozen object is also, by definition, sealed — Object.isSealed() returns true for anything that passed through Object.freeze(). The reverse isn’t true: a sealed object is not automatically frozen, since its values can still change.
The shallow-freeze trap
Both methods are shallow. They lock the object’s own top-level properties, but if one of those properties holds a reference to another object or array, that nested object is completely unaffected.
const config = Object.freeze({
name: "app",
limits: { maxUsers: 100 },
});
config.name = "renamed"; // blocked, freeze works here
config.limits.maxUsers = 99999; // succeeds — limits object isn't frozen
This trips people up constantly: Object.freeze() looks like it should make an object immutable end to end, but it only ever freezes one level deep. A “deep freeze” requires recursively walking the object and calling Object.freeze() on every nested object and array yourself, or reaching for a library that does it for you.
Where these fit versus const
const and Object.freeze() solve different problems, and mixing them up is a common source of confusion. See var vs let vs const for how const behaves: it prevents reassigning the variable binding — you can’t point a const variable at a different object — but it does nothing to stop you from mutating the object that variable points to. Object.freeze() is the other way around: the variable can still be reassigned if it isn’t const, but the object’s own contents can’t be changed.
const user = { name: "Ada" };
user.name = "Grace"; // works fine — const doesn't stop this
user = {}; // TypeError — const stops this
Combining const obj = Object.freeze({...}) gets you both guarantees at once: the binding can’t be repointed, and the object’s own properties can’t be mutated.
When to actually use them
Object.freeze() is most useful for values that represent fixed configuration or constants shared across a codebase — an enum-like object, a set of feature flags loaded once at startup, or any object passed into code you don’t fully trust and don’t want silently mutated. It’s also a cheap guard in state-management patterns that assume state is never mutated directly, since a frozen state object will loudly fail (in strict mode) if something tries to bypass the intended update path — a useful complement to TypeScript’s readonly modifiers, which catch the same mistake at compile time but do nothing at runtime.
Object.seal() sees far less real-world use. It’s occasionally useful for objects with a known, fixed shape — you want to guarantee no code accidentally adds or typos a new property — while still allowing legitimate updates to the values that are there. In practice, most codebases that reach for immutability guarantees reach for freeze() directly rather than stopping at seal().
Neither method is a performance optimization in typical application code, though V8 and other engines can take advantage of a frozen object’s shape stability in hot paths. Reach for them for correctness and safety guarantees, not speed.
The takeaway
Object.seal() locks an object’s shape — no new or deleted properties — while leaving existing values writable; Object.freeze() locks the shape and the values, and is the one you’ll reach for in practice far more often. Both are shallow, so nested objects and arrays need their own explicit freeze if you want immutability all the way down, and both pair naturally with const to lock the variable binding too.
Tagged
Keep reading
Takina · · 4 min read Object.groupBy() in JavaScript, Explained
Object.groupBy() buckets array items by a key you compute, replacing the reduce() boilerplate developers have written for years. How it works.
Takina · · 4 min read Web Locks API: Coordinating Work Across Browser Tabs
The Web Locks API lets JavaScript acquire named locks shared across tabs, so only one tab does work like a token refresh or a write at a time.
Takina · · 4 min read Svelte 5 Runes Explained
Svelte 5 runes like $state and $derived replace the old reactive-assignment magic with explicit function calls that work anywhere in a file.