| |

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:

OperatorAssigns when left isEquivalent to
||=Falsya || (a = b)
&&=Truthya && (a = b)
??=Nullisha ?? (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:

OperatorIntent
||=“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:

Taskif wayLogical assignment
Default if nullishif (a == null) a = ba ??= b
Default if falsyif (!a) a = ba ||= b
Update if truthyif (a) a = ba &&= 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

OperatorAssigns when left isEquivalent to
||=Falsya || (a = b)
&&=Truthya && (a = b)
??=Nullisha ?? (a = b)

Truthy and Falsy

Falsy valuesTruthy values
falsetrue
0, -0Any non-zero number
''Any non-empty string
nullObjects
undefinedArrays
NaNFunctions

Nullish Values

NullishNot nullish
null0
undefined''
false
NaN

Behavior Table

Value||=&&=??=
nullassignskipassign
undefinedassignskipassign
0assignskipskip
''assignskipskip
falseassignskipskip
NaNassignskipskip
'x'skipassignskip
42skipassignskip
trueskipassignskip
{}skipassignskip

vs Traditional

PatternTraditionalLogical assignment
Falsy defaulta = a || ba ||= b
Truthy updatea = a && ba &&= b
Nullish defaulta = a ?? ba ??= b
With ifif (!a) a = ba ||= b

Use Cases

PatternOperator
Config defaults??=
Lazy init??=
Memoization??=
Conditional update&&=
Fallback for falsy||=
Named defaults||=

Gotchas

GotchaSolution
||= clobbers 0Use ??=
||= clobbers ''Use ??=
||= clobbers falseUse ??=
const can’t reassignUse let
Chaining operatorsAdd parentheses
Short-circuit means side effects skippedUse 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

PitfallProblemSolution
||= on 0ClobbersUse ??=
||= on ''ClobbersUse ??=
||= on falseClobbersUse ??=
&&= on 0SkipsUse ??= if checking existence
const reassignmentTypeErrorUse let
Missing parenthesesUnclear precedenceAdd parens
Target not lvalueSyntaxErrorAssign step by step
?. chained with ??=Wrong shapeBreak into two lines
Expecting type validationWrong typesValidate 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

OperatorAssigns whenUse case
||=Left is falsyFalsy default
&&=Left is truthyConditional update
??=Left is nullishNull-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
  • ||= clobbers 0, '', and false — use ??= when those are valid
  • &&= checks truthiness, not existence — 0 and '' skip the assignment
  • These operators work on let and var — not const (reassignment)
  • Chained operators should be parenthesized for clarity
  • Assignment target must be an lvalueobj?.a?.b ??= x won’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!