ESC

Type to search the knowledge base.

Logical Assignment Operators

||=, &&=, and ??= — assign only when nullish or falsy/truthy, with short-circuiting and practical defaults patterns.

intermediate3 min read
  • javascript
  • logical-assignment
  • nullish
  • operators

Logical assignment combines a logical operator with assignment: update the left-hand side only when a condition holds. They’re small, but they remove a class of noisy if defaults — and they short-circuit so the right-hand side may never run.

The three

// OR assignment — assign if left is falsy
x ||= y; // x || (x = y)

// AND assignment — assign if left is truthy
x &&= y; // x && (x = y)

// Nullish assignment — assign if left is null or undefined
x ??= y; // x ?? (x = y)

??= for defaults (usually what you want)

function createUser(options) {
  const opts = { ...options };
  opts.theme ??= 'light';
  opts.pageSize ??= 20;
  return opts;
}

createUser({ pageSize: 0 });
// pageSize stays 0 — ?? does not treat 0 as missing

Compare with ||=:

let count = 0;
count ||= 10; // becomes 10 — often a bug for counters
count = 0;
count ??= 10; // stays 0

Use ??= for “fill in if nullish.” Use ||= only when you truly mean falsy (rare for numbers/strings).

||= patterns

// cache init
cache[key] ||= compute(key);

// note: if compute returns a falsy value, next access recomputes
// prefer ??= if empty string / 0 / false are valid cached values
element.dataset.ready ||= 'true';

&&= patterns

// only overwrite if already present/truthy
user.name &&= user.name.trim();

// feature flag style
settings.debug &&= env.allowDebug;

Less common than ??=, but neat for “refine existing value.”

Short-circuiting side effects

let n = 1;
n ??= expensive(); // expensive NOT called

let m = 0;
m ||= expensive(); // called, result assigned

let p = null;
p ??= expensive(); // called

Same short-circuit rules as ||, &&, ??.

With object properties and optional chaining

// legal: assignable reference on the left
opts.retry ??= 3;

// NOT valid: optional chain is not assignable
// opts?.retry ??= 3; // SyntaxError

The left-hand side must be an assignable target (variable, property, element).

Destructuring and params still better sometimes

function f({ limit = 20 } = {}) {
  /* ... */
}
// vs
function f(opts = {}) {
  opts.limit ??= 20;
}

Parameter defaults and destructuring defaults are clearer for function APIs; logical assignment shines for incremental config mutation and caches.

Interview answer

“||= assigns when the left side is falsy, &&= when truthy, ??= when null or undefined. ??= is the right defaulting operator when 0 and ‘’ are valid. Right-hand sides short-circuit and may not evaluate. Left-hand side must be assignable—no opts?.x ??=.”

Nested defaults without clobbering

function normalizeConfig(input) {
  const cfg = { ...input };
  cfg.ui ??= {};
  cfg.ui.theme ??= 'light';
  cfg.ui.density ??= 'comfortable';
  cfg.api ??= {};
  cfg.api.retries ??= 3;
  return cfg;
}

??= at each level fills holes without wiping user-provided nested objects. Avoid cfg.ui = cfg.ui || {} when ui could legally be a provided empty object you still want to keep as the same reference for tests. Logical assignment keeps the existing object identity when present.

Further reading

Related guides