Assigning to a property of an object returned by Object.freeze() sometimes fails silently and sometimes throws a TypeError. What decides which one happens?
answer
- the write always fails either way
- only the reporting differs
- depends on the writing code, not the object
- modules and class bodies are strict
- some built-ins throw regardless
basics
~20 sStrict mode decides. A failed write to a frozen object's property throws a TypeError in strict-mode code and is silently ignored in sloppy mode. ES modules and class bodies are always strict, so the same line behaves differently across files.
solid answer
~50 sThe failing operation is identical in both cases — the property is non-writable, so the internal `[[Set]]` returns `false` — but the *reporting* depends on the strictness of the code doing the assignment. In sloppy mode the failure is discarded and execution continues with the old value; in strict mode the engine throws a `TypeError`. The same rule applies to `delete` on a sealed object's property, which returns `false` in sloppy mode and throws in strict mode. This matters because strictness is a property of the *calling* code, not of the frozen object: a helper in a plain `<script>` will swallow the write, while the identical helper inside an ES module or a class body throws, since both are strict by default. That is why frozen objects are a good early-warning device in modern module code and nearly useless in legacy sloppy scripts.
code
javascript · 12 linesconst o = Object.freeze({ a: 1 });
function sloppyWrite() { o.a = 2; return o.a; }
function strictWrite() { 'use strict'; o.a = 2; return o.a; }
console.log(sloppyWrite()); // 1 - assignment silently discarded
console.log(o.a = 5, o.a); // 5 1 - expression value vs stored value
try { strictWrite(); } catch (e) { console.log(e.name); } // TypeError
const arr = Object.freeze([1, 2]);
try { arr.push(3); } catch (e) { console.log('push:', e.name); } // push: TypeErrorgo deeper
Know that a write to a frozen object never changes the value, and that whether you see an error depends on strict mode. Recall that ES module files are strict without any directive.
Explain the mechanism: the internal set returns false, and the assignment's throw flag — the strictness of the code doing the write — decides between silence and TypeError. Mention that delete behaves the same way.
Demonstrate the debugging angle: when a frozen config appears mutable, locate the writing context and check whether it is a sloppy script. Know that built-ins like push throw regardless, and that assertions must test isFrozen rather than read-back.
Frame it as an enforcement question — runtime freezing only reports failures where the writing code is strict, so decide deliberately whether the codebase relies on modules-everywhere, on lint rules, or on defensive copying instead.
## The two halves of the answer A frozen object's data properties have `writable: false`. Any assignment goes through the object's internal `[[Set]]` method, which validates the write, refuses it, and returns `false`. That much is fixed — the write never lands, regardless of mode. What varies is what the *assignment expression* does with that `false`. The spec's evaluation of `obj.prop = value` ends with an operation that takes a "throw" flag, and that flag is the strictness of the code containing the assignment: - **sloppy mode** — the `false` is discarded, the expression evaluates to the assigned value, and execution continues. The property still holds its old value. - **strict mode** — a `TypeError` is thrown, typically worded like "Cannot assign to read only property 'x' of object". ```js const o = Object.freeze({ a: 1 }); function sloppy() { o.a = 2; return o.a; } function strict() { 'use strict'; o.a = 2; return o.a; } sloppy(); // 1 — the write vanished strict(); // TypeError ``` Note the expression's *value* is still the right-hand side in sloppy mode: `console.log(o.a = 2)` prints `2` while `o.a` remains `1`. That mismatch is what makes silent failure so confusing to debug. ## Strictness comes from the caller, not the object This is the part interviews probe. The frozen object carries no strictness of its own; the mode is a static property of the source text performing the write. In modern code that text is usually strict without anyone opting in: - **ES modules** are always strict — an `import`/`export` file needs no `'use strict'`. - **Class bodies** are always strict, including methods and static blocks. - A classic `<script>` or a CommonJS file without the directive is sloppy. So the same utility function, copied from a bundled module into an inline script, changes from throwing to swallowing. When someone reports "our frozen config is being mutated and nothing complains", the first question is which of these contexts the writing code lives in. ## The same split for `delete` Sealing and freezing both make properties non-configurable, so `delete obj.prop` fails. The reporting rule is identical: the `delete` operator evaluates to `false` in sloppy mode and throws a `TypeError` in strict mode. Code that branches on `if (delete obj.k)` is therefore a rare place where the sloppy behaviour is actually useful — everywhere else it hides the bug. ## Built-in methods can throw in both modes A useful exception: some standard-library algorithms perform a *throwing* write internally, independent of the caller's mode. `Array.prototype.push` is the canonical case — it uses the throwing form of `Set`, so pushing onto a frozen array throws a `TypeError` even from sloppy code, while a bare `arr[0] = 1` on the same array fails silently there. ```js const arr = Object.freeze([1, 2]); arr[0] = 9; // sloppy: ignored; strict: TypeError arr.push(3); // TypeError in BOTH modes ``` The rule of thumb: your own assignment syntax obeys your mode; a built-in that internally uses `CreateDataPropertyOrThrow` or a throwing `Set` obeys the spec text, not you. `Object.defineProperty` on a frozen object likewise always throws, because it is a function that reports failure by throwing rather than an assignment expression. ## Why the design is like this Sloppy-mode silence is a backward-compatibility artefact: before ES5 introduced property attributes, assignments could not fail, so ES5 kept failed writes non-throwing in existing code and made strict mode the opt-in that surfaces them. Strict mode turns roughly a dozen such silent failures — writes to non-writable properties, deletes of non-configurable ones, assignments to undeclared variables — into errors. ## Practical consequences 1. **Freeze is only a guard rail where the writers are strict.** In an all-ESM codebase, freezing exported constants converts an entire class of accidental mutation into a loud failure at the exact call site. 2. **Never assert immutability by reading back.** A test that does `frozen.a = 2; expect(frozen.a).toBe(1)` passes in sloppy mode for the wrong reason and throws in strict mode. Assert with `Object.isFrozen(obj)` instead, or wrap the write in `expect(() => { ... }).toThrow(TypeError)` under strict test files. 3. **Silent failure is worse than either alternative** when values quietly diverge from what the code appears to set. If you rely on freeze for correctness, make sure the writing code is strict — practically, that means modules.
- Why does push on a frozen array throw even from sloppy-mode code?Because the failure is not raised by your assignment syntax. `Array.prototype.push` internally uses the throwing form of `Set` (and `CreateDataPropertyOrThrow` for the new index), so the spec algorithm itself throws when the target is non-extensible or `length` is non-writable. Your caller's mode only governs assignments written in your own source text, so `arr[0] = 9` stays silent while `arr.push(3)` throws.
- How would you write a test that a configuration object really is immutable?Assert the state, not the symptom: `expect(Object.isFrozen(config)).toBe(true)`, and recursively for nested objects if you deep-froze. If you want to assert the failure behaviour, put the write inside `expect(() => { config.a = 1; }).toThrow(TypeError)` in a strict test file. Reading the value back after a write proves nothing in sloppy mode — the read succeeds because the write was discarded.
- Does Object.defineProperty on a frozen object follow the same silent-versus-throwing rule?No — it always throws a `TypeError`, in both modes. The mode-dependent rule applies to assignment expressions and the `delete` operator, which have a throw flag driven by the surrounding code's strictness. `Object.defineProperty` is an ordinary function whose only failure channel is an exception. `Reflect.defineProperty` is the boolean-returning alternative when you want to test rather than throw.
saying these in an interview costs you the question
- Claiming the write still succeeds in sloppy mode
- Thinking the frozen object itself decides strictness
- Saying freeze always throws on assignment
- Believing 'use strict' inside a module changes anything
- Testing immutability by reading the value back