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— assignbtoaonly ifais falsy (0,"",null,undefined,NaN,false).a &&= b— assignbtoaonly ifais truthy.a ??= b— assignbtoaonly ifaisnullorundefined(any other value, including0or"", is left alone).
What they really expand to
// ||= 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
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.
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
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
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
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
if (opts.timeout === undefined || opts.timeout === null) {
opts.timeout = 5000;
}
opts.retries = opts.retries || 3;
if (user.token) {
user.token = encodeURIComponent(user.token);
}After
opts.timeout ??= 5000; opts.retries ||= 3; user.token &&= encodeURIComponent(user.token);