JavaScript 57 🧬 Logical assignment ||=, &&=, ??=
let a = null;
a ||= 'default';
console.log(a);
let b = 0;
b ||= 5;
console.log(b);
let c = 'value';
c ||= 'other';
console.log(c);
let d = null;
d &&= 'assigned';
console.log(d);
let e = 'truthy';
e &&= 'updated';
console.log(e);
let f = 0;
f &&= 99;
console.log(f);
let g = null;
g ??= 'fallback';
console.log(g);
let h = 0;
h ??= 42;
console.log(h);
let i = '';
i ??= 'empty';
console.log(i);
let config = {};
config.host ??= 'localhost';
config.port ??= 8080;
console.log(config);
let user = { name: null };
user.name ??= 'Anonymous';
console.log(user.name);
The logical assignment operators combine a logical check with assignment. They let you write a ||= b instead of a || (a = b), and the result is shorter, clearer, and avoids double-evaluation of a. Three operators: ||=, &&=, and ??= — each with a specific purpose.
Key point: These operators only assign when the left side meets the condition — falsy for ||=, truthy for &&=, nullish for ??=. If the condition isn’t met, the assignment is skipped entirely — the right side isn’t even evaluated.
a – What are logical assignment operators
Logical assignment operators were introduced in ES2021. They combine a logical check with an assignment — three operators in total.
The three operators:
| Operator | Assigns when left is | Equivalent to |
|---|---|---|
||= | Falsy | a || (a = b) |
&&= | Truthy | a && (a = b) |
??= | Nullish | a ?? (a = b) |
Why they exist:
Before these operators, you’d write:
if (!a) a = b;
Or:
a = a || b;
The first is verbose. The second always assigns — even when the value doesn’t change — and re-evaluates a. Logical assignment fixes both: it’s short, it only assigns when needed, and it doesn’t re-evaluate.
||= — assign if falsy:
let a = null;
a ||= 'default';
console.log(a);
// [ 'default' ]
The left side is null (falsy), so it’s replaced.
let b = 0;
b ||= 5;
console.log(b);
// [ 5 ]
0 is falsy, so it’s replaced. This is the classic gotcha — ||= treats 0, '', false, and NaN as “empty” just like ||.
&&= — assign if truthy:
let a = 'truthy';
a &&= 'updated';
console.log(a);
// [ 'updated' ]
The left side is truthy, so it’s replaced.
let b = null;
b &&= 'updated';
console.log(b);
// [ null ]
The left side is falsy, so the assignment is skipped. b stays null.
??= — assign if nullish:
let a = null;
a ??= 'fallback';
console.log(a);
// [ 'fallback' ]
The left side is nullish, so it’s replaced.
let b = 0;
b ??= 42;
console.log(b);
// [ 0 ]
0 isn’t nullish, so it’s preserved. This is the big difference from ||=.
Comparison table:
| Left side | ||= assigns? | &&= assigns? | ??= assigns? |
|---|---|---|---|
null | ✅ | ❌ | ✅ |
undefined | ✅ | ❌ | ✅ |
0 | ✅ | ❌ | ❌ |
'' | ✅ | ❌ | ❌ |
false | ✅ | ❌ | ❌ |
NaN | ✅ | ❌ | ❌ |
'value' | ❌ | ✅ | ❌ |
42 | ❌ | ✅ | ❌ |
true | ❌ | ✅ | ❌ |
{} | ❌ | ✅ | ❌ |
[] | ❌ | ✅ | ❌ |
When each is used:
| Operator | Intent |
|---|---|
||= | “Set a default if the current value is falsy” |
&&= | “Update the value only if it exists” |
??= | “Set a default if the current value is null/undefined” |
Short-circuit behavior — right side isn’t evaluated:
let count = 0;
let calls = 0;
function expensive() {
calls++;
return 99;
}
count ||= expensive();
console.log(count); // 99
console.log(calls); // 1
count = 100;
count ||= expensive();
console.log(count); // 100
console.log(calls); // 1 ← still 1, not called again
The right side only runs when the assignment happens. This matters for expensive expressions or side-effectful function calls.
Why the difference matters — || vs ??:
// ❌ ||= clobbers 0, '', false
let port = 0;
port ||= 8080;
console.log(port);
// [ 8080 ]
// ✅ ??= preserves them
let port2 = 0;
port2 ??= 8080;
console.log(port2);
// [ 0 ]
This is the reason ??= was added — to fix the accidental clobbering that ||= does for valid falsy values.
b – Using logical assignment in practice
These operators show up most often in config, defaults, and lazy initialization.
Pattern 1 — Config defaults:
function configure(options = {}) {
options.host ??= 'localhost';
options.port ??= 8080;
options.timeout ??= 30000;
return options;
}
console.log(configure({ port: 0 }));
// [ { port: 0, host: 'localhost', timeout: 30000 } ]
The 0 for port is preserved — ??= is the right operator.
Pattern 2 — Lazy initialization:
let instance = null;
function getInstance() {
instance ??= createExpensiveInstance();
return instance;
}
getInstance() creates the instance only once.
Pattern 3 — Memoization cache:
const cache = new Map();
function memoize(key, compute) {
if (!cache.has(key)) {
cache.set(key, compute());
}
return cache.get(key);
}
Or with a nullish pattern:
const cache = {};
function memoize(key, compute) {
return cache[key] ??= compute();
}
cache[key] ??= compute() — assigns only if the cached value is nullish.
Pattern 4 — Accumulators with ||=, &&=, ??=:
let query = '';
function addFilter(filter) {
query ||= '?';
query += filter + '&';
}
Pattern 5 — Default object properties:
const user = {};
user.name ??= 'Anonymous';
user.role ??= 'guest';
user.active ??= false;
console.log(user);
// [ { name: 'Anonymous', role: 'guest', active: false } ]
Pattern 6 — Optional chaining setup:
let options = null;
options ??= {};
options.timeout ??= 5000;
Pattern 7 — Conditional update with `&&=:
let session = null;
function extend(newExpiry) {
session &&= { ...session, expiry: newExpiry };
}
Only updates the session if it exists — otherwise leaves null.
Pattern 8 — Array length guard:
let arr = [];
arr[0] ??= 'first';
console.log(arr);
// [ [ 'first' ] ]
Pattern 9 — Filling missing object fields:
function normalizeConfig(cfg) {
cfg.name ??= 'app';
cfg.version ??= '1.0.0';
cfg.debug ??= false;
return cfg;
}
Pattern 10 — Named defaults with ||=:
let displayName = '';
function ensureName() {
displayName ||= 'User' + Date.now();
}
Here ||= is intentional — we want a default even if the string is empty.
Pattern 11 — Combining ?. with ??=:
const config = { server: {} };
config.server?.host ??= 'localhost';
console.log(config.server.host);
// [ 'localhost' ]
Pattern 12 — Chained defaults:
let value;
value ??= getA() ?? getB() ?? 'final';
Each ??= triggers if the value is still nullish.
Pattern 13 — Class field initializer pattern:
class Config {
constructor(options) {
options ??= {};
this.host = options.host ?? 'localhost';
this.port = options.port ?? 3000;
}
}
Pattern 14 — Counter without overwrite:
let count = null;
function increment() {
count ??= 0;
count++;
}
Pattern 15 — Guards in loops:
const seen = new Map();
for (const item of items) {
seen[item.id] ??= item;
}
First occurrence wins.
Pattern 16 — Merging into an existing object:
function applyDefaults(obj, defaults) {
for (const key in defaults) {
obj[key] ??= defaults[key];
}
return obj;
}
Pattern 17 — Environment variables:
process.env.NODE_ENV ??= 'development';
process.env.PORT ??= '3000';
Pattern 18 — Conditional mutations with `&&=:
let user = { name: 'Alice' };
user &&= { ...user, visited: true };
console.log(user);
// [ { name: 'Alice', visited: true } ]
user = null;
user &&= { ...user, visited: true };
console.log(user);
// [ null ]
Pattern 19 — Optional chain with logical assignment:
const obj = {};
obj.nested ??= {};
obj.nested.value ??= 42;
console.log(obj.nested.value);
// [ 42 ]
Pattern 20 — Full config example:
function buildConfig(user) {
const config = {};
config.theme ??= user?.preferences?.theme;
config.theme ??= 'light';
config.fontSize ??= user?.preferences?.fontSize;
config.fontSize ??= 14;
config.debug &&= user.isDeveloper; // only if already set
return config;
}
console.log(buildConfig({}));
// [ { theme: 'light', fontSize: 14 } ]
console.log(buildConfig({ preferences: { theme: 'dark' } }));
// [ { theme: 'dark', fontSize: 14 } ]
Comparison with if statements:
| Task | if way | Logical assignment |
|---|---|---|
| Default if nullish | if (a == null) a = b | a ??= b |
| Default if falsy | if (!a) a = b | a ||= b |
| Update if truthy | if (a) a = b | a &&= b |
The logical assignment form is shorter and reads as “a or-assign b.”
Gotcha — logical assignment doesn’t short-circuit the left side’s side effects:
let obj = { get count() { console.log('get'); return 0; } };
obj.count ??= 5;
// [ get ] ← getter is called
// [ get ] ← called again during assignment? No — only once
Actually the left side is evaluated only once. The ??= doesn’t re-trigger the getter. This is one of its advantages over a = a ?? b.
When NOT to use logical assignment:
- When the right side has side effects you always want
- When you need to always assign (use
=) - When the logic is complex — the operators only do one check
c – Pitfalls and edge cases
Logical assignment looks simple but has a few sharp edges.
Pitfall 1 — ||= treats 0 and '' as empty:
let count = 0;
count ||= 10;
console.log(count);
// [ 10 ] ← 0 was clobbered
If 0 is meaningful, use ??=:
let count = 0;
count ??= 10;
console.log(count);
// [ 0 ]
Pitfall 2 — &&= doesn’t check for existence:
let count = 0;
count &&= 10;
console.log(count);
// [ 0 ] ← 0 is falsy, so no assignment
&&= uses truthiness, not existence. For nullish checks, use ??=.
Pitfall 3 — Chaining different operators:
a ||= b ??= c;
// SyntaxError? No — but confusing
Chaining without parentheses works, but it’s confusing. Use parentheses:
a ||= (b ??= c);
Pitfall 4 — Mixing with + and other arithmetic:
let total = 0;
total ||= getInitialValue() + 5;
The right side is a single expression — that’s fine. But if you forget parentheses, precedence can surprise you:
let x = null;
x ??= 5 + 3;
console.log(x);
// [ 8 ]
Pitfall 5 — Side effects run once:
let calls = 0;
function sideEffect() { calls++; return 'x'; }
let a = null;
a ??= sideEffect();
// calls = 1
a ??= sideEffect();
// calls = 1 ← already set, sideEffect not called
Pitfall 6 — Reassignment of const:
const a = null;
a ??= 'value';
// TypeError: Assignment to constant variable
Logical assignment is still assignment — it doesn’t work on const.
Pitfall 7 — Object property with getter:
const obj = {
_value: null,
get value() { return this._value; },
set value(v) { this._value = v; }
};
obj.value ??= 'default';
console.log(obj.value);
// [ 'default' ]
Getters and setters are invoked as expected.
Pitfall 8 — Destructuring after ??=:
let config = null;
config ??= { port: 3000 };
const { port } = config;
console.log(port);
// [ 3000 ]
Pitfall 9 — Nullish assignment doesn’t validate:
let user = {};
user.name ??= 42; // assigns 42 even though we wanted a string
??= doesn’t type-check. Use it carefully with dynamic data.
Pitfall 10 — Reading before assignment is required:
obj?.nested ??= {};
This works because obj?.nested returns undefined when nested is missing, and ??= assigns.
But:
obj.nested.deep ??= {};
// TypeError: Cannot read properties of undefined (reading 'deep')
The ?. was missing at the intermediate step. Chain them:
obj?.nested?.deep ??= {};
Wait — that’s wrong. ??= can’t be applied to obj?.nested?.deep if the intermediate is undefined — but the assignment target must be a valid lvalue.
// ✅ Correct
obj.nested ??= {};
obj.nested.deep ??= {};
The general rule: ??= needs a valid assignment target. obj?.nested ??= {} works because obj.nested is a property access on obj — it’s a valid lvalue. But obj?.nested?.deep ??= {} doesn’t because obj?.nested?.deep isn’t a straightforward property.
Pitfall 11 — Combining logical assignment with delete:
let obj = { a: null };
delete obj.a;
console.log(obj.a);
// [ undefined ]
obj.a ??= 'restored';
console.log(obj.a);
// [ 'restored' ]
Pitfall 12 — In class methods:
class Foo {
constructor() {
this.cache ??= new Map();
}
}
Works fine — this.cache is a valid assignment target.
Pitfall 13 — Chain fallbacks with ??=:
let value;
value ??= a ?? b ?? c;
console.log(value);
If value is nullish, the right side evaluates — a ?? b ?? c picks the first non-nullish. Then value is assigned that.
Pitfall 14 — Short-circuit and logic:
let a = 0;
let b = 0;
a ??= ++b;
console.log(a, b);
// [ 0, 0 ] ← 0 isn't nullish, so ++b never ran
Pitfall 15 — Assignment inside conditionals:
if (user.name ??= 'default') {
// always truthy — non-empty string
}
Comparison with if — readability:
// Verbose
if (options.host == null) {
options.host = 'localhost';
}
// Concise
options.host ??= 'localhost';
The second reads: “host defaults to localhost if missing.”
Common use case — building a config:
const config = {};
config.host ??= process.env.HOST ?? 'localhost';
config.port ??= Number(process.env.PORT) || 3000; // careful — || here
config.debug ??= process.env.DEBUG === 'true';
console.log(config);
Notice the || inside — since we want to fall back if the parsed port is 0 or NaN, and || handles that. Nesting logical operators is fine as long as you know which check you want.
When NOT to use logical assignment:
- Always assigning: use
= - Always checking and assigning: use
if - Complex condition: use
if - Logging or side effects every time: use
if
Logical assignment is a shorthand — not a replacement for all assignments.
Complete Example Session
// ============================================
// PART 1: ||= ON NULL
// ============================================
let a = null;
a ||= 'default';
console.log(a);
// [ 'default' ]
// ============================================
// PART 2: ||= ON ZERO
// ============================================
let b = 0;
b ||= 5;
console.log(b);
// [ 5 ]
// ============================================
// PART 3: ||= ON TRUTHY
// ============================================
let c = 'value';
c ||= 'other';
console.log(c);
// [ 'value' ]
// ============================================
// PART 4: &&= ON NULL
// ============================================
let d = null;
d &&= 'assigned';
console.log(d);
// [ null ]
// ============================================
// PART 5: &&= ON TRUTHY
// ============================================
let e = 'truthy';
e &&= 'updated';
console.log(e);
// [ 'updated' ]
// ============================================
// PART 6: &&= ON ZERO
// ============================================
let f = 0;
f &&= 99;
console.log(f);
// [ 0 ]
// ============================================
// PART 7: ??= ON NULL
// ============================================
let g = null;
g ??= 'fallback';
console.log(g);
// [ 'fallback' ]
// ============================================
// PART 8: ??= ON ZERO
// ============================================
let h = 0;
h ??= 42;
console.log(h);
// [ 0 ]
// ============================================
// PART 9: ??= ON EMPTY STRING
// ============================================
let i = '';
i ??= 'empty';
console.log(i);
// [ '' ]
// ============================================
// PART 10: CONFIG OBJECT
// ============================================
let config = {};
config.host ??= 'localhost';
config.port ??= 8080;
console.log(config);
// [ { host: 'localhost', port: 8080 } ]
// ============================================
// PART 11: OVERWRITE NAME
// ============================================
let user = { name: null };
user.name ??= 'Anonymous';
console.log(user.name);
// [ 'Anonymous' ]
// ============================================
// PART 12: SHORT-CIRCUIT
// ============================================
let calls = 0;
function sideEffect() { calls++; return 'x'; }
let x = 'set';
x ??= sideEffect();
console.log(calls);
// [ 0 ]
let y = null;
y ??= sideEffect();
console.log(calls);
// [ 1 ]
// ============================================
// PART 13: LAZY INIT
// ============================================
let instance = null;
function getInstance() {
instance ??= { id: 1 };
return instance;
}
console.log(getInstance() === getInstance());
// [ true ]
// ============================================
// PART 14: MEMOIZATION
// ============================================
const cache = {};
function memo(key, compute) {
return cache[key] ??= compute();
}
console.log(memo('a', () => 42));
// [ 42 ]
console.log(memo('a', () => 99));
// [ 42 ] ← from cache
// ============================================
// PART 15: &&= FOR CONDITIONAL UPDATE
// ============================================
let session = { user: 'Alice' };
session &&= { ...session, lastSeen: Date.now() };
console.log(typeof session.lastSeen);
// [ 'number' ]
session = null;
session &&= { ...session, lastSeen: Date.now() };
console.log(session);
// [ null ]
// ============================================
// PART 16: CHAINED ?. WITH ??=
// ============================================
const obj = {};
obj.nested ??= {};
obj.nested.value ??= 42;
console.log(obj.nested.value);
// [ 42 ]
// ============================================
// PART 17: CONST CANNOT REASSIGN
// ============================================
const fixed = 1;
try {
fixed ??= 2;
} catch (err) {
console.log(err.message);
}
// [ Assignment to constant variable. ]
// ============================================
// PART 18: GETTER / SETTER
// ============================================
const holder = {
_value: null,
get value() { return this._value; },
set value(v) { this._value = v; }
};
holder.value ??= 'default';
console.log(holder.value);
// [ 'default' ]
// ============================================
// PART 19: LOOPING WITH ??=
// ============================================
const seen = {};
const items = [
{ id: 'a', value: 1 },
{ id: 'a', value: 2 },
{ id: 'b', value: 3 }
];
for (const item of items) {
seen[item.id] ??= item;
}
console.log(seen.a.value);
// [ 1 ] ← first wins
// ============================================
// PART 20: FULL SCRIPT
// ============================================
let a57 = null;
a57 ||= 'default';
console.log(a57);
let b57 = 0;
b57 ||= 5;
console.log(b57);
let c57 = 'value';
c57 ||= 'other';
console.log(c57);
let d57 = null;
d57 &&= 'assigned';
console.log(d57);
let e57 = 'truthy';
e57 &&= 'updated';
console.log(e57);
let f57 = 0;
f57 &&= 99;
console.log(f57);
let g57 = null;
g57 ??= 'fallback';
console.log(g57);
let h57 = 0;
h57 ??= 42;
console.log(h57);
let i57 = '';
i57 ??= 'empty';
console.log(i57);
let config57 = {};
config57.host ??= 'localhost';
config57.port ??= 8080;
console.log(config57);
let user57 = { name: null };
user57.name ??= 'Anonymous';
console.log(user57.name);
Quick Reference
The Three Operators
| Operator | Assigns when left is | Equivalent to |
|---|---|---|
||= | Falsy | a || (a = b) |
&&= | Truthy | a && (a = b) |
??= | Nullish | a ?? (a = b) |
Truthy and Falsy
| Falsy values | Truthy values |
|---|---|
false | true |
0, -0 | Any non-zero number |
'' | Any non-empty string |
null | Objects |
undefined | Arrays |
NaN | Functions |
Nullish Values
| Nullish | Not nullish |
|---|---|
null | 0 |
undefined | '' |
false | |
NaN |
Behavior Table
| Value | ||= | &&= | ??= |
|---|---|---|---|
null | assign | skip | assign |
undefined | assign | skip | assign |
0 | assign | skip | skip |
'' | assign | skip | skip |
false | assign | skip | skip |
NaN | assign | skip | skip |
'x' | skip | assign | skip |
42 | skip | assign | skip |
true | skip | assign | skip |
{} | skip | assign | skip |
vs Traditional
| Pattern | Traditional | Logical assignment |
|---|---|---|
| Falsy default | a = a || b | a ||= b |
| Truthy update | a = a && b | a &&= b |
| Nullish default | a = a ?? b | a ??= b |
| With if | if (!a) a = b | a ||= b |
Use Cases
| Pattern | Operator |
|---|---|
| Config defaults | ??= |
| Lazy init | ??= |
| Memoization | ??= |
| Conditional update | &&= |
| Fallback for falsy | ||= |
| Named defaults | ||= |
Gotchas
| Gotcha | Solution |
|---|---|
||= clobbers 0 | Use ??= |
||= clobbers '' | Use ??= |
||= clobbers false | Use ??= |
const can’t reassign | Use let |
| Chaining operators | Add parentheses |
| Short-circuit means side effects skipped | Use if when always needed |
Best Practices
✅ Do This:
// Use ??= for defaults (preserves falsy)
options.port ??= 3000; // ✅
// Use ??= for lazy init
instance ??= createInstance(); // ✅
// Use ??= for memoization
cache[key] ??= compute(); // ✅
// Use ||= for falsy defaults (intentional)
displayName ||= 'Guest'; // ✅
// Use &&= for conditional update
session &&= { ...session, updated: true }; // ✅
// Use ??= in config builders
config.host ??= 'localhost'; // ✅
// Chain with ?. for safe access
obj?.nested ??= {}; // ✅
// Ensure let, not const
let value = null;
value ??= 'x'; // ✅
❌ Don’t Do This:
// Don't use ||= when 0, '', false are valid
port ||= 8080; // ❌ clobbers 0
port ??= 8080; // ✅
// Don't use &&= thinking it checks for null
let x = 0;
x &&= 5; // ⚠️ stays 0
// Don't chain without parentheses
a ||= b ??= c; // ⚠️ unclear
a ||= (b ??= c); // ✅
// Don't use on const
const x = null;
x ??= 5; // ❌ TypeError
// Don't rely on side effects being called every time
value ??= expensiveCall(); // ⚠️ only on nullish
// Don't forget assignment target must be an lvalue
obj?.a?.b ??= 5; // ⚠️ wrong shape
obj.a ??= {};
obj.a.b ??= 5; // ✅
// Don't use for validation
user.age ??= 18; // ⚠️ no type check
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
||= on 0 | Clobbers | Use ??= |
||= on '' | Clobbers | Use ??= |
||= on false | Clobbers | Use ??= |
&&= on 0 | Skips | Use ??= if checking existence |
const reassignment | TypeError | Use let |
| Missing parentheses | Unclear precedence | Add parens |
| Target not lvalue | SyntaxError | Assign step by step |
?. chained with ??= | Wrong shape | Break into two lines |
| Expecting type validation | Wrong types | Validate first |
Real-World Examples
1. ||= on Null
let a = null;
a ||= 'default';
console.log(a);
// [ 'default' ]
2. ||= on Zero
let b = 0;
b ||= 5;
console.log(b);
// [ 5 ]
3. ||= on Truthy
let c = 'value';
c ||= 'other';
console.log(c);
// [ 'value' ]
4. &&= on Truthy
let d = 'truthy';
d &&= 'updated';
console.log(d);
// [ 'updated' ]
5. &&= on Falsy
let e = null;
e &&= 'never';
console.log(e);
// [ null ]
6. ??= on Null
let f = null;
f ??= 'fallback';
console.log(f);
// [ 'fallback' ]
7. ??= on Zero
let g = 0;
g ??= 42;
console.log(g);
// [ 0 ]
8. ??= on Empty String
let h = '';
h ??= 'empty';
console.log(h);
// [ '' ]
9. Config Defaults
const config = {};
config.host ??= 'localhost';
config.port ??= 8080;
console.log(config);
// [ { host: 'localhost', port: 8080 } ]
10. Preserve Zero
const config = { port: 0 };
config.port ??= 8080;
console.log(config.port);
// [ 0 ]
11. Lazy Init
let instance = null;
function get() {
instance ??= { id: 1 };
return instance;
}
console.log(get() === get());
// [ true ]
12. Memoization
const cache = {};
function memo(k, fn) {
return cache[k] ??= fn();
}
console.log(memo('a', () => 42));
// [ 42 ]
console.log(memo('a', () => 99));
// [ 42 ]
13. Conditional Update
let session = { user: 'Alice' };
session &&= { ...session, updated: true };
console.log(session.updated);
// [ true ]
14. Skip Update on Null
let session = null;
session &&= { ...session, updated: true };
console.log(session);
// [ null ]
15. Chained Nullish
const obj = {};
obj.nested ??= {};
obj.nested.value ??= 42;
console.log(obj.nested.value);
// [ 42 ]
16. Getter / Setter
const holder = {
_v: null,
get v() { return this._v; },
set v(x) { this._v = x; }
};
holder.v ??= 'default';
console.log(holder.v);
// [ 'default' ]
17. Const Fails
const c = null;
try {
c ??= 'x';
} catch (err) {
console.log(err.message);
}
// [ Assignment to constant variable. ]
18. Short-Circuit
let calls = 0;
function fn() { calls++; return 'x'; }
let x = 'set';
x ??= fn();
console.log(calls);
// [ 0 ]
let y = null;
y ??= fn();
console.log(calls);
// [ 1 ]
19. First Wins in Loop
const seen = {};
const items = [{ id: 'a', v: 1 }, { id: 'a', v: 2 }];
for (const item of items) seen[item.id] ??= item;
console.log(seen.a.v);
// [ 1 ]
20. Full Script
let a57 = null;
a57 ||= 'default';
console.log(a57);
let b57 = 0;
b57 ||= 5;
console.log(b57);
let c57 = 'value';
c57 ||= 'other';
console.log(c57);
let d57 = null;
d57 &&= 'assigned';
console.log(d57);
let e57 = 'truthy';
e57 &&= 'updated';
console.log(e57);
let f57 = 0;
f57 &&= 99;
console.log(f57);
let g57 = null;
g57 ??= 'fallback';
console.log(g57);
let h57 = 0;
h57 ??= 42;
console.log(h57);
let i57 = '';
i57 ??= 'empty';
console.log(i57);
let config57 = {};
config57.host ??= 'localhost';
config57.port ??= 8080;
console.log(config57);
let user57 = { name: null };
user57.name ??= 'Anonymous';
console.log(user57.name);
Visual: The Three Operators
┌──────────────────────────────────────────────┐
│ ||= │
│ │
│ a || (a = b) │
│ │
│ Assign if a is FALSY │
│ │
│ null, undefined, 0, '', false, NaN → set │
│ 'x', 42, true, {} → keep │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ &&= │
│ │
│ a && (a = b) │
│ │
│ Assign if a is TRUTHY │
│ │
│ 'x', 42, true, {} → set │
│ null, undefined, 0, '', false, NaN → keep │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ ??= │
│ │
│ a ?? (a = b) │
│ │
│ Assign if a is NULLISH │
│ │
│ null, undefined → set │
│ 0, '', false, NaN, 'x' → keep │
│ │
└──────────────────────────────────────────────┘
Visual: ||= vs ??=
┌──────────────────────────────────────────────┐
│ let port = 0; │
│ │
│ port ||= 8080 │
│ → 0 is falsy → set → 8080 │
│ │
│ port ??= 8080 │
│ → 0 is not nullish → keep → 0 │
│ │
│ Choose the right operator for the intent │
│ │
└──────────────────────────────────────────────┘
Visual: Short-Circuit
┌──────────────────────────────────────────────┐
│ a ??= expensiveCall() │
│ │
│ If a is nullish: │
│ expensiveCall() runs │
│ a is assigned result │
│ │
│ If a is set (0, '', false, 'x', etc.): │
│ expensiveCall() NOT called │
│ a unchanged │
│ │
│ Side effects are conditional │
│ │
└──────────────────────────────────────────────┘
Summary
| Operator | Assigns when | Use case |
|---|---|---|
||= | Left is falsy | Falsy default |
&&= | Left is truthy | Conditional update |
??= | Left is nullish | Null-safe default |
Key takeaways:
||=assigns a default when the left side is falsy — like||, but in-place&&=assigns when the left side is truthy — good for conditional updates??=assigns when the left side is nullish — the safest default operator- All three short-circuit — the right side isn’t evaluated when no assignment happens
||=clobbers0,'', andfalse— use??=when those are valid&&=checks truthiness, not existence —0and''skip the assignment- These operators work on
letandvar— notconst(reassignment) - Chained operators should be parenthesized for clarity
- Assignment target must be an lvalue —
obj?.a?.b ??= xwon’t work directly - Use
??=for config defaults, lazy init, memoization, first-wins loops - Use
||=for falsy defaults where you specifically want to replace empty values - Use
&&=for optional updates that only apply when a value exists
Remember: Logical assignment is a = a op b compressed into a op= b, with the added benefit of evaluating the left side once and short-circuiting the right. Use ??= for null-safe defaults — the most common pattern in modern code. Use ||= only when you explicitly want falsy replacement. Use &&= for conditional mutation. And never forget the difference between falsy and nullish — that’s where the bugs hide.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!