skip to content

In JavaScript, how do the logical assignment operators `&&=`, `||=` and `??=` differ from writing `x = x && y`, and why does that difference matter for a property with a setter?

level: middleimportance: should knowfreq 44%

answer

  1. not the same as reassigning the same value
  2. no write means no side effect
  3. setters and traps stay silent
  4. the target reference evaluates once
  5. one test each: falsy, truthy, nullish

basics

~20 s

Logical assignment short-circuits the assignment itself: when the test fails no write happens at all. So x ||= y writes only if x is falsy, x &&= y only if x is truthy, x ??= y only if x is null or undefined — and setters, proxy traps and frozen-object errors are skipped entirely.

solid answer

~50 s

`x ||= y`, `x &&= y` and `x ??= y` are not sugar for `x = x || y`. The unconditional form always performs a write, even when the value is unchanged; the logical forms evaluate the left reference, apply the test, and if the test says "keep what you have" they return the existing value without performing any assignment at all. That is observable whenever writing is not free: a setter does not fire, a `Proxy` `set` trap does not run, a frozen or read-only property does not throw in strict mode, and a framework's change tracking sees no mutation. The tests match their non-assigning cousins — `||=` writes on falsy, `&&=` writes on truthy, `??=` writes only on `null` or `undefined` — so `opts.retries ??= 3` is the correct idiom for filling in a default without clobbering a legitimate `0`. All three arrived in ES2021.

code

javascript · 13 lines
javascript
const target = {
  _v: 0,
  get v() { return this._v; },
  set v(next) { console.log('set called'); this._v = next; }
};

target.v ||= 5;   // logs 'set called' — 0 is falsy, so a write happens
target.v ??= 9;   // logs nothing  — 5 is not nullish, no write at all
console.log(target.v); // 5

const opts = { retries: 0 };
opts.retries ??= 3;
console.log(opts.retries); // 0 — a deliberate zero is preserved

go deeper

for a junior

Know the three tests: ||= assigns when the left side is falsy, &&= when it is truthy, and ??= only when it is null or undefined — and that ??= is the safe way to fill in a default.

for a middle

Explain that the assignment itself is short-circuited, so no write occurs when the current value is kept, and prove it with a setter, a frozen property in strict mode, or a Proxy set trap.

for a senior

Show awareness of the systems consequences: no write means no change notification, no dirty flag, no persistence hook and no strict-mode error, which can be exactly what you want or a bug depending on whether downstream code observes writes.

for a principal

Be ready to set a convention: whether defaults are applied by mutating incoming option objects at all, versus normalising them into a fresh, validated object at the boundary so that no downstream code depends on when a write did or did not happen.

## The three operators ES2021 added three compound assignment operators that combine a logical test with an assignment: ```js x ||= y; // assign y if x is falsy x &&= y; // assign y if x is truthy x ??= y; // assign y if x is null or undefined ``` Each mirrors the test of the corresponding binary operator: `||` on falsiness, `&&` on truthiness, `??` on nullishness. ## They short-circuit the WRITE, not just the value The usual explanation — "`x ||= y` is short for `x = x || y`" — is wrong in a way interviewers like to probe. The specification evaluates the left-hand reference, reads its value, applies the test, and *returns early without performing a Set* when the test says the current value stands. Only when the test passes is the right operand evaluated and the assignment performed. With `x = x || y` there is always a write, even if the value written is identical to the one already there. That distinction is invisible for a plain variable and very visible for anything where writing has behaviour attached. ### A setter ```js const target = { _v: 0, get v() { return this._v; }, set v(next) { console.log('set called'); this._v = next; } }; target.v ||= 5; // logs 'set called' — 0 is falsy, so it writes target.v ??= 9; // logs nothing — 5 is not nullish, no write happens console.log(target.v); // 5 ``` ### A Proxy A `Proxy` with a `set` trap sees the same thing: the `get` trap runs to read the current value, but the `set` trap is only invoked when the assignment actually happens. Anything built on write interception — change tracking, dirty flags, persistence, reactivity — therefore behaves differently between `obj.a ||= v` and `obj.a = obj.a || v`. ### A frozen or non-writable property In strict mode (which includes all module code and class bodies), assigning to a non-writable property throws a `TypeError`. Logical assignment skips that when it short-circuits: ```js 'use strict'; const o = Object.freeze({ a: 1 }); o.a ||= 2; // no error: 1 is truthy, so no write is attempted o.a = o.a || 2; // TypeError: Cannot assign to read only property 'a' ``` ## The right operand is lazy too As with the binary operators, the right-hand expression is only evaluated when it is needed: ```js cache.value ??= buildExpensiveThing(); // builder runs at most once ``` That makes `??=` a compact memoisation idiom, provided the computed value is never itself `null` or `undefined` — if it can be, the guard will re-run every call and you need an explicit sentinel or a `has`-style check instead. ## Choosing among the three - `??=` fills in defaults for options and config: `opts.retries ??= 3` preserves a deliberate `0`. - `||=` fills in defaults where any falsy value counts as missing, e.g. replacing an empty label. - `&&=` is the rarest: it updates a value only if there is already one there, such as `user.token &&= refresh(user.token)` — refresh only what exists. ## Evaluation of the target happens once The reference on the left is evaluated a single time, which matters when it has side effects or is expensive: ```js arr[index()] ??= compute(); // index() is called once arr[index()] = arr[index()] ?? compute(); // index() called twice ``` This is the same benefit compound operators like `+=` have always had, and it is a correctness issue, not just a performance one, when the subscript expression is impure. ## Interaction with the mixing restriction Just as `??` cannot be mixed with `||` or `&&` in one unparenthesised expression, there is no operator that combines two of these tests. If a default must apply to both `null` and the empty string, write the condition out explicitly rather than stacking operators. ## What to say in an interview Give the three tests, then immediately make the point that stands out: the assignment itself short-circuits, so no `Set` occurs when the value is kept. Back it with the setter or frozen-object example — that is the observable proof the desugaring "`x = x || y`" is not exact.

  • Does `obj.x ||= v` trigger a Proxy `set` trap when `obj.x` is already truthy?
    No. The operator reads the current value — so the `get` trap does run — applies the truthiness test, and returns without performing an assignment, so the `set` trap is never invoked. Anything built on write interception, such as dirty tracking or persistence, sees no mutation at all.
  • Why is `cache.value ??= compute()` a memoisation idiom, and where does it break?
    The right operand is only evaluated when the left is null or undefined, so `compute()` runs at most once and the result is cached in place. It breaks if `compute()` can itself return `null` or `undefined`: the guard never becomes satisfied, so the work repeats on every call. Use an explicit presence flag or a `Map` with `has` there.
  • When is `&&=` actually the right operator?
    When you want to transform a value only if one is already present — for example `session.token &&= refresh(session.token)`, which refreshes an existing token but leaves an absent one absent. It is the least used of the three precisely because "update only if set" is a narrower need than "fill in if missing".

saying these in an interview costs you the question

  • Says x ||= y is exactly x = x || y
  • Thinks the setter runs even when the test short-circuits
  • Claims ??= assigns whenever the value is falsy
  • Believes the right operand is always evaluated
  • Assumes the target expression is evaluated twice

context