Reflect.defineProperty and Reflect.set return booleans where Object.defineProperty and a plain assignment throw. Why does Reflect report failure that way, and what does it mean for calling code?
answer
- traps must return a boolean
- status code, not an exception
- refused is not the same as malformed
- false in strict mode too, silently
- check the return or you lose the write
basics
~20 sReflect's mutating methods return true or false because Proxy traps are required to return a boolean success flag, so the mirror API had to speak the same protocol. The cost is that a failure is silent unless the caller checks the returned value.
solid answer
~40 sThe `Reflect` methods that change an object — `set`, `defineProperty`, `deleteProperty`, `preventExtensions`, `setPrototypeOf` — all return a boolean saying whether the operation succeeded. Their older counterparts do something else: `Object.defineProperty` throws a `TypeError` on failure and returns the object on success, and a plain assignment fails silently in sloppy mode but throws in strict mode. `Reflect` is shaped this way because it mirrors the `Proxy` trap protocol, where every mutating trap must return a boolean, and because a status code lets you branch without a `try`/`catch`. The practical consequence is real: `Reflect.set(frozenObj, 'a', 2)` returns `false` and does nothing, with no exception even in strict mode. If you ignore the return value you have written a silent no-op. Treat these calls like any other status-returning API — check the result, or assert on it.
code
javascript · 12 lines'use strict';
const obj = Object.freeze({ a: 1 });
console.log(Reflect.set(obj, 'a', 2)); // false
console.log(Reflect.defineProperty(obj, 'b', { value: 3 })); // false
console.log(Reflect.deleteProperty(obj, 'a')); // false
try { obj.a = 2; }
catch (e) { console.log(e.constructor.name); } // "TypeError"
try { Object.defineProperty(obj, 'b', { value: 3 }); }
catch (e) { console.log(e.constructor.name); } // "TypeError"go deeper
Know that these Reflect methods hand back true or false, and that ignoring that value means a failed write leaves no trace. Their Object counterparts throw instead.
Explain the reason for the split — traps must return a boolean, so the default implementations had to as well — and distinguish a refused operation, which returns false, from a malformed call, which still throws.
Show the shipped bug: a set trap forwarding by assignment returns the assigned value, so writing 0 or '' is read as failure and throws through the proxy in strict mode. Argue for checking the boolean in any generic property-copying code.
Own the choice between the two protocols across a codebase: exceptions where a failed mutation is a defect and must halt, status codes where refusal is an expected state to be handled. Mixing them without a rule is how silent no-ops accumulate.
## Two failure protocols in one language JavaScript has never had one way to report a failed object mutation. Depending on which surface you use, the same failed operation may throw, return false, or do nothing at all: ```js 'use strict'; const obj = Object.freeze({ a: 1 }); obj.a = 2; // TypeError (strict) / silent no-op (sloppy) Object.defineProperty(obj, 'b', { value: 3 }); // TypeError, always delete obj.a; // TypeError (strict) / false (sloppy) ``` `Reflect` collapses all of that into one rule: **the method returns a boolean, and never throws for a merely-refused operation.** ```js Reflect.set(obj, 'a', 2); // false Reflect.defineProperty(obj, 'b', { value: 3 }); // false Reflect.deleteProperty(obj, 'a'); // false ``` Note the word *refused*. `Reflect` still throws `TypeError` for a genuinely malformed call — a primitive where an object is required, an invalid descriptor, a non-callable target. What it does not do is throw because the object said no. ## Why this shape was chosen The direct reason is the `Proxy` trap protocol. A `set`, `defineProperty`, `deleteProperty`, `preventExtensions` or `setPrototypeOf` trap must return a value that is coerced to a boolean, and returning a falsy value means "this operation failed". So a trap forwarding to the default implementation needs a default implementation that *returns a boolean* — which is exactly what `Reflect` provides: ```js const handler = { set(t, key, value, receiver) { if (key.startsWith('_')) return false; // refuse return Reflect.set(t, key, value, receiver); // forward, boolean and all } }; ``` Write that trap as `target[key] = value` instead and you return the assigned *value*. That happens to work for truthy values and breaks the day someone assigns `0`, `''`, `null` or `false` — the proxy then reports failure, and strict-mode assignment through the proxy throws a `TypeError` for a write that actually succeeded. This is a real and frequently-shipped bug. The second reason is ergonomics at the meta layer. Reflective code often *expects* some operations to fail — sealing, freezing, and non-configurable properties are all normal conditions, not exceptional ones. Wrapping every call in `try`/`catch` to distinguish "refused" from "broken" is both noisy and imprecise, because the `catch` also swallows genuine programming errors. ```js if (!Reflect.defineProperty(obj, key, desc)) { logger.warn(`could not define ${String(key)}`); } ``` ## The trap: silence The flip side is that `Reflect` will let you write a no-op and never tell you. In strict-mode application code, `obj.a = 2` on a frozen object throws and you find out in the first test run. `Reflect.set(obj, 'a', 2)` returns `false`, and if you discarded the value, nothing anywhere records that the write was lost. Static analysis rarely flags an unused return value, so this survives review easily. The discipline is simple: any `Reflect` call whose success you assume should either have its result checked, or be paired with an assertion in code you control. Where you actually want an exception, `Object.defineProperty` is still there and still throws — the two APIs are complementary rather than one replacing the other. ## What each pairing does - `Reflect.set` vs assignment — assignment throws in strict mode, is silent in sloppy mode; `Reflect.set` returns a boolean in both. - `Reflect.defineProperty` vs `Object.defineProperty` — the latter returns the object on success and throws on failure; the former returns true or false. - `Reflect.deleteProperty` vs `delete` — `delete` returns false in sloppy mode but throws in strict mode for a non-configurable property; `Reflect.deleteProperty` returns false either way. - `Reflect.setPrototypeOf` vs `Object.setPrototypeOf` — the latter throws if the prototype is not extensible or immutable; the former returns false. - `Reflect.preventExtensions` vs `Object.preventExtensions` — the latter returns the object; the former returns a boolean, which can be false when a proxy's trap refuses. ## Choosing between them Use `Object.*` in ordinary application code where a failed mutation is a bug and you want it to blow up loudly. Use `Reflect.*` inside traps, wrappers, and generic property-copying machinery where refusal is an expected outcome you intend to handle — and then actually handle it.
- Does Reflect.set ever throw, then?Yes, but only for a malformed call rather than a refused operation: a non-object target throws a TypeError, and a setter that itself throws propagates normally. The boolean is reserved for "the object declined the write" — a frozen or non-writable property, or a proxy trap returning false.
- What goes wrong if a Proxy set trap forwards with `target[key] = value` instead of Reflect.set?The trap returns the assigned value rather than a success boolean. Assign a falsy value — 0, '', null, false — and the proxy reads that as failure, so a strict-mode assignment through the proxy throws a TypeError even though the write succeeded. Forwarding with Reflect.set returns the correct boolean.
- How would you make a Reflect-based helper fail loudly instead?Check the return value and throw yourself, with a message naming the key and the reason: `if (!Reflect.defineProperty(o, k, d)) throw new TypeError(...)`. That keeps the branch explicit and gives a better diagnostic than the built-in TypeError, which does not always say which invariant was violated.
saying these in an interview costs you the question
- Reflect.set throws on a frozen object in strict mode
- A false return means the argument was invalid
- Object.defineProperty returns true when it succeeds
- Ignoring the boolean is safe because failures are impossible
- Returning the assigned value from a set trap is equivalent