skip to content

In JavaScript, an assignment such as `obj.total = 5` appears to do nothing — reading `obj.total` afterwards still gives the old value and no error is reported. What conditions on the prototype chain cause this, and how would you confirm which one applies?

level: seniorimportance: should knowfreq 36%

answer

  1. writes consult the chain before landing
  2. a getter with no setter refuses the value
  3. strict mode turns silence into a TypeError
  4. descriptor on the prototype names the cause
  5. defineProperty ignores the chain entirely

basics

~20 s

Assignment consults the prototype chain first. An inherited accessor with no setter, or an inherited non-writable data property, makes the write fail — silently in sloppy mode, with a TypeError in strict mode. An inherited setter can also absorb the value without storing it.

solid answer

~40 s

Plain assignment runs the internal [[Set]] operation, which walks the prototype chain before creating anything. Three inherited shapes divert it: an accessor with only a getter, and a non-writable data property, both make the write fail — ignored in sloppy mode, `TypeError` in strict mode, which includes all module and class code; and an accessor **with** a setter runs that setter with `this` bound to your object, so whatever it does or does not store is what you observe. To diagnose, first check `Object.hasOwn(obj, 'total')` — false confirms the write never landed. Then walk up with `Object.getPrototypeOf` and inspect `Object.getOwnPropertyDescriptor(proto, 'total')`: a descriptor with `get` and no `set` or with `writable: false` names the cause. `Object.defineProperty(obj, 'total', { value: 5 })` bypasses the chain entirely and defines an own property regardless.

code

javascript · 20 lines
javascript
'use strict';
const proto = { get total() { return 0; } };
const obj = Object.create(proto);

try {
  obj.total = 5;                 // getter-only accessor on the chain
} catch (e) {
  console.log(e.constructor.name); // 'TypeError'
}

console.log(Object.hasOwn(obj, 'total')); // false — nothing landed

// find the culprit
let o = obj;
while (o && !Object.hasOwn(o, 'total')) o = Object.getPrototypeOf(o);
console.log(Object.getOwnPropertyDescriptor(o, 'total').set); // undefined

// bypass the chain deliberately
Object.defineProperty(obj, 'total', { value: 5, writable: true });
console.log(obj.total);          // 5

go deeper

for a junior

Know that assignment usually creates a property on the object, but that a getter or a read-only property inherited from a prototype can stop it, and that strict mode reports the failure.

for a middle

Explain the [[Set]] path precisely: the chain is searched, and a getter-only accessor, a non-writable data property, or a setter each change the outcome. Name the sloppy-versus-strict difference.

for a senior

Demonstrate the diagnosis: confirm with an own-property check, walk the chain, read the descriptor, and re-run under strict mode to surface the TypeError. Say when bypassing with defineProperty is legitimate rather than reflexive.

for a principal

Own the API consequence: exposing getter-only or frozen properties on a shared prototype constrains every consumer downstream, so decide deliberately whether that invariant is worth the silent-failure surface it creates and how it is documented.

## Why an assignment can be a no-op The common summary "reads walk the chain, writes do not" is true about *where the value lands*, but not about what the write consults. `obj.total = 5` invokes [[Set]] with `obj` as the receiver, and before creating an own property the engine looks for an existing `total` along the chain. What it finds decides the outcome. ## Case 1: an inherited getter-only accessor ```js const proto = { get total() { return 0; } }; const obj = Object.create(proto); obj.total = 5; obj.total; // 0 Object.hasOwn(obj, 'total'); // false ``` An accessor whose descriptor has `get` but no `set` cannot receive a value, and the specification does not fall back to creating an own property. In sloppy mode the assignment is discarded silently. In strict mode it throws: ```js 'use strict'; obj.total = 5; // TypeError: Cannot set property total ``` Since ES modules and class bodies are always strict, the same code often throws in one file and stays silent in another — which is exactly why the bug survives so long in scripts. ## Case 2: an inherited non-writable data property ```js const proto = {}; Object.defineProperty(proto, 'total', { value: 0, writable: false }); const obj = Object.create(proto); obj.total = 5; // ignored (sloppy) / TypeError (strict) ``` A non-writable property on the chain blocks assignment on every object below it. This surprises people who expect the inherited property merely to be shadowed, and it is one reason `Object.freeze(proto)` has effects reaching well beyond the frozen object itself: freezing makes every data property non-writable, so instances below can no longer assign those names. ## Case 3: an inherited setter that absorbs the value ```js const proto = { set total(v) { /* validation that rejects odd inputs */ }, get total() { return 0; } }; const obj = Object.create(proto); obj.total = 5; Object.hasOwn(obj, 'total'); // false — no own property created ``` Here the write did something: the setter ran, with `this` bound to `obj`, not to the prototype. If the setter stores nothing, validates and drops the value, or writes to a differently named backing field, the observable result is again "the assignment did nothing". No error is thrown in any mode, because the write was accepted. ## The diagnosis sequence 1. **Did the write land?** `Object.hasOwn(obj, 'total')`. If `true`, the write succeeded and your problem is elsewhere. If `false`, the chain intervened. 2. **Who owns the name?** Walk it: ```js let o = obj; while (o && !Object.hasOwn(o, 'total')) o = Object.getPrototypeOf(o); Object.getOwnPropertyDescriptor(o, 'total'); ``` 3. **Read the descriptor.** `{ get, set: undefined }` is case 1. `{ writable: false }` is case 2. `{ get, set }` with a function is case 3 — read the setter's body. 4. **Re-run in strict mode.** Under strict mode cases 1 and 2 throw a `TypeError` with the property name, turning a silent bug into a stack trace. This is a strong argument for modules and `'use strict'` in any remaining scripts. ## The escape hatch, and when it is wrong `Object.defineProperty(obj, 'total', { value: 5, writable: true, enumerable: true, configurable: true })` performs [[DefineOwnProperty]] on the target directly. It never walks the chain, never triggers an inherited setter, and is not blocked by a non-writable inherited property. `Reflect.defineProperty` is the same operation returning a boolean instead of throwing. Be deliberate about using it. If the prototype exposes a getter-only property or a setter that validates, that is usually an intentional contract, and forcing an own property past it breaks the invariant the author was protecting. Use it when you genuinely own both sides — for instance when a base object was frozen for safety and a derived object legitimately needs its own value. Note also that anything built on ordinary assignment behaves the same way: `Object.assign(target, source)` and `Reflect.set(target, key, value)` both use [[Set]], so they too can be diverted by an inherited setter or blocked by a non-writable inherited property. Copying with `Object.assign` is therefore not a reliable way to force values onto an object whose prototype constrains them.

  • Why does the same assignment throw in one file and stay silent in another?
    Because the failure mode depends on strictness. A failed [[Set]] is ignored in sloppy mode and throws a `TypeError` in strict mode, and ES module code and class bodies are always strict. The same helper pasted into a classic script silently swallows the bug, which is a strong reason to keep everything in modules.
  • Does `Object.assign` get around an inherited setter?
    No. `Object.assign` copies with ordinary [[Set]] semantics on the target, so an inherited setter runs and a non-writable inherited property blocks the copy exactly as a direct assignment would. `Reflect.set` behaves the same. Only `Object.defineProperty` or `Reflect.defineProperty` define on the target without consulting the chain.
  • When an inherited setter runs, what is `this` inside it, and why does that matter?
    `this` is the receiver — the object you assigned to — not the prototype that owns the setter. That is what lets one accessor defined once on a prototype maintain per-instance backing state. If the setter writes `this._total`, the value lands as an own property of the instance under a different name than the one you assigned.
  • Why does freezing a prototype affect objects that were never frozen?
    `Object.freeze` makes every own data property of the prototype non-writable, and a non-writable property on the chain blocks assignment to that name on every object below it. So instances remain extensible in general yet can no longer assign the frozen names, which reads as a mysterious no-op until you inspect the descriptor.

saying these in an interview costs you the question

  • Says assignment never looks at the prototype chain
  • Thinks a getter-only inherited property just gets shadowed
  • Assumes a failed assignment always throws
  • Believes Object.assign bypasses inherited setters
  • Says the inherited setter runs with this bound to the prototype

context