JavaScriptLogical Assignment (&&=, ||=, ??=)

Logical Assignment (&&=, ||=, ??=)

Three small operators that landed in ES2021 — &&=, ||= and ??= — combine a logical check with an assignment. They are short, they short-circuit (the right side is only evaluated if needed), and they make a lot of "set this only when…" code one line shorter.

The three operators in one breath
  • a ||= b — assign b to a only if a is falsy (0, "", null, undefined, NaN, false).

  • a &&= b — assign b to a only if a is truthy.

  • a ??= b — assign b to a only if a is null or undefined (any other value, including 0 or "", is left alone).

What they really expand to

JS
// ||=
a ||= b;
// is the same as:
a || (a = b);

// &&=
a &&= b;
// is the same as:
a && (a = b);

// ??=
a ??= b;
// is the same as:
a ?? (a = b);

That equivalence is important: the assignment only runs when the left side matches the condition. They are not the same as a = a || b — the simpler form would assign even when no change is needed, which can trigger setters or trip up reactive frameworks.

||= — fill in a missing fallback

JS
function applyDefaults(opts) {
  opts.timeout ||= 5000;
  opts.retries ||= 3;
  return opts;
}

console.log(applyDefaults({ timeout: 1000 }));
console.log(applyDefaults({}));
{ timeout: 1000, retries: 3 }
{ timeout: 5000, retries: 3 }

Be careful — ||= treats 0 and "" as "missing". If a caller passes { timeout: 0 } you would overwrite it.

??= — fall back only for null/undefined

This is usually the safer choice for "set if not provided", because real values like 0 and the empty string stay untouched.

JS
function applyDefaults(opts) {
  opts.timeout ??= 5000;
  opts.retries ??= 3;
  opts.label   ??= "untitled";
  return opts;
}

console.log(applyDefaults({ timeout: 0, label: "" }));
{ timeout: 0, label: "", retries: 3 }

Here timeout: 0 and label: "" were left alone because they are not null or undefined.

&&= — only update if there is something to update

JS
const user = { name: "Ada", token: "abc123" };

// Re-encode the token if it exists, otherwise leave it alone.
user.token &&= encodeURIComponent(user.token);

// Strip whitespace only when there is a string to strip.
user.bio &&= user.bio.trim();

console.log(user);
{ name: 'Ada', token: 'abc123' }

&&= is the rarest of the three. It is useful when you want to transform a property only if it already has a value.

Building up a cache or registry

JS
const cache = {};

function getValue(key) {
  // First call: cache[key] is undefined, so we compute and store.
  cache[key] ??= computeExpensive(key);
  return cache[key];
}

function computeExpensive(key) {
  console.log("computing for", key);
  return key.toUpperCase();
}

getValue("hello");
getValue("hello");
getValue("world");
computing for hello
computing for world
Nested objects

JS
const state = {};

// Initialise lazy collections.
state.errors ??= [];
state.errors.push("validation failed");

state.byUser ??= {};
state.byUser["ada"] ??= { messages: [] };
state.byUser["ada"].messages.push("hi");

console.log(state);
{ errors: [ 'validation failed' ], byUser: { ada: { messages: [ 'hi' ] } } }
vs the traditional ways

Before

JS
if (opts.timeout === undefined || opts.timeout === null) {
  opts.timeout = 5000;
}

opts.retries = opts.retries || 3;

if (user.token) {
  user.token = encodeURIComponent(user.token);
}

After

JS
opts.timeout ??= 5000;
opts.retries ||= 3;
user.token   &&= encodeURIComponent(user.token);
Short-circuit really is short-circuit
In `a ||= b`, the expression `b` is **not** evaluated when `a` is truthy. That matters when `b` is an expensive call or has side effects — `cache[key] ??= computeExpensive(key)` does not call `computeExpensive` on cache hits.
Picking between || and ??
Use `??=` when `0`, `""` and `false` are valid values you want to keep. Use `||=` when "any falsy value counts as missing" is what you actually mean. Mixing them up is a classic bug source.