skip to content

Why does assigning to a property of an object that was passed to Object.freeze() do nothing at all in sloppy mode, yet throw a TypeError under "use strict"?

level: middleimportance: should knowfreq 48%

answer

  1. the write fails either way
  2. a boolean result nobody reads
  3. strict mode reports, it does not prevent
  4. frozen, sealed, getter-only, non-extensible
  5. freeze is shallow, so nesting still mutates

basics

~10 s

The assignment fails in both dialects. Sloppy mode discards the failure and continues; strict mode reports it as a TypeError. Strict mode changes the reporting of a failed write, never whether the write succeeds.

solid answer

~50 s

The write itself fails either way — a frozen object's properties are non-writable, so the internal `[[Set]]` operation returns false. What differs is what the language does with that false. In sloppy mode the result is thrown away and execution continues on the next line, which is why the object looks unchanged and no one notices. In strict mode a failed `[[Set]]` is turned into a `TypeError` at the assignment site. The same rule covers the whole family of silent write failures: assigning to a non-writable data property, assigning to an accessor that has only a getter, adding a property to an object that has been sealed or given `Object.preventExtensions`, and `delete` of a non-configurable property. This is the practical reason to prefer strict code — a typo against an immutable config surfaces immediately instead of turning into a value that never changed.

code

javascript · 11 lines
javascript
"use strict";

const config = Object.freeze({ retries: 3 });

try {
  config.retries = 5;
} catch (e) {
  console.log(e instanceof TypeError);
}

console.log(config.retries);

go deeper

for a junior

Recall that writing to a frozen object quietly does nothing in ordinary script code and throws a TypeError once "use strict" is on, and that the value never changes in either case.

for a middle

Explain the mechanism: the internal set operation returns a boolean, sloppy mode discards it, strict mode converts false into a TypeError. Be able to list the other failing writes — non-writable, getter-only, non-extensible.

for a senior

Show how this changes debugging in production: a frozen config that diverges from expectation is invisible in sloppy code, and you can describe how you would surface it — strictness, defineProperty attributes, or a real immutable structure.

for a principal

Weigh whether freezing plus strict mode is the right immutability guarantee for a shared object graph at all, given that freeze is shallow and cost-bearing, versus structural sharing or a copy-on-write discipline enforced at the API boundary.

## The write fails in both dialects The first thing to get straight is what strict mode does *not* do. It does not make frozen objects writable, and it does not make sloppy code succeed where strict code fails. In both dialects the assignment fails. The dialects differ only in whether that failure is reported. Under the hood, every property assignment `obj.p = v` runs an internal operation the specification calls `[[Set]]`, which returns a boolean: did the write happen? For an ordinary object with a writable data property, it writes and returns true. When the target property is non-writable, or the object is non-extensible and the property does not exist yet, or the property is an accessor with no setter, `[[Set]]` returns false without changing anything. The assignment expression then looks at that boolean. In sloppy code it is discarded. In strict code, a false result is converted into a `TypeError` thrown at the assignment. ```javascript "use strict"; const config = Object.freeze({ retries: 3 }); config.retries = 5; // TypeError: Cannot assign to read only property 'retries' ``` Drop the directive and the last line is a no-op: no error, no change, and `config.retries` is still `3`. ## The whole family of silent failures Freezing is only the most familiar case. The same strict-vs-sloppy split applies to every operation whose internal result is a boolean the language can ignore: - **Non-writable data property.** `Object.defineProperty(o, 'x', { value: 1, writable: false })`, then `o.x = 2`. - **Getter-only accessor.** A property defined with `get` and no `set`; assigning to it is a failed write, not a call. - **Non-extensible object.** After `Object.preventExtensions(o)` or `Object.seal(o)`, adding a *new* property fails. Note the difference from freezing: sealing still allows existing properties to be updated. - **`delete` of a non-configurable property.** `delete Object.prototype` returns `false` in sloppy mode and throws `TypeError` in strict mode. - **Writes through the prototype chain.** If a prototype holds a non-writable `x`, assigning `child.x = 1` fails rather than shadowing it on the child — a genuinely surprising one, and strict mode is how you find out. - **Some built-in globals.** `undefined`, `NaN` and `Infinity` are non-writable properties of the global object. ## Why the sloppy behaviour existed Early JavaScript had no way to *make* a property non-writable from script, so failed writes were rare and mostly involved host objects. Throwing would have broken pages, so returning quietly was the safe design. ES5 introduced `Object.defineProperty` and `Object.freeze` and, with them, a realistic way to write to something immutable by accident — which is exactly when a silent failure becomes a real bug and strict mode becomes worth having. ## Where this bites in practice The classic case is a frozen configuration or constants object. A team freezes `config` to document that it is immutable; somewhere else a code path writes `config.timeout = 5000` and then reads it back later expecting the new value. In sloppy mode this is a silent divergence — the write appears to work because nothing complains, and the bug shows up far away as "the timeout is wrong". In strict mode the stack trace points at the offending line. A close relative is a typo against an accessor-only object: an API exposes `get status()` with no setter, a caller writes `obj.status = 'done'`, and in sloppy mode the value simply evaporates. ## What strict mode does *not* catch here Two important limits. First, `Object.freeze` is shallow. Nested objects stay mutable, so `frozen.nested.x = 1` succeeds in both dialects and throws nothing: ```javascript "use strict"; const o = Object.freeze({ nested: { x: 0 } }); o.nested.x = 1; // fine — the inner object was never frozen console.log(o.nested.x); // 1 ``` Second, strict mode reports failures, it does not prevent mutation in general. Array methods such as `push` on a frozen array throw for the same underlying reason (they perform a failed `[[Set]]` or `[[DefineOwnProperty]]`), but ordinary mutation of a non-frozen object is not affected by strict mode at all. ## How to talk about it A strong answer separates three layers: the property attribute or object-level restriction that makes the write fail, the internal boolean that records the failure, and the dialect that decides whether that boolean becomes an exception. Candidates who say "strict mode makes objects immutable" have collapsed all three, and the follow-up — "then why does `frozen.nested.x = 1` still work?" — usually exposes it.

  • Does strict mode change the outcome of `frozen.nested.value = 1` when `frozen` was created by `Object.freeze({ nested: {} })`?
    No. Freezing is shallow, so the inner object is an ordinary mutable object and the write genuinely succeeds in both dialects. Nothing failed, so there is no failure to report. This is the standard follow-up that separates "strict mode reports failed writes" from the wrong mental model "strict mode makes objects immutable".
  • Besides assignment, which other operation flips from a silent false to a thrown TypeError under strict mode?
    `delete` is the clearest one. Deleting a non-configurable property — `delete Object.prototype`, for instance — evaluates to `false` in sloppy mode and throws a `TypeError` in strict mode. Strict mode also makes `delete someVariable` on a plain identifier a parse-time `SyntaxError`, which is a different mechanism: an early error rather than a converted failure result.
  • Why is assigning to a property that a prototype declares non-writable a failure rather than shadowing it on the child object?
    Because assignment runs `[[Set]]`, which walks the prototype chain first. When it finds a non-writable data property, the operation returns false instead of creating an own property on the receiver — shadowing only happens when the inherited property is writable. Sloppy mode swallows that false, so the child silently keeps reading the prototype's value; strict mode throws a `TypeError`.

saying these in an interview costs you the question

  • Says strict mode makes frozen objects actually immutable
  • Thinks the write succeeds in sloppy mode
  • Claims freeze is deep, so nested writes throw too
  • Confuses seal with freeze on existing properties
  • Expects a ReferenceError rather than a TypeError

context