Logical Assignment Operators
||=, &&=, and ??= — assign only when nullish or falsy/truthy, with short-circuiting and practical defaults patterns.
- 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 ??=.”
Related
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
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.