Logical Assignment Operators in JS Complete Guide

A complete guide to JavaScript logical assignment operators. Covers ||=, &&=, and ??= syntax, how each differs from regular assignment, short-circuit behavior, practical default-filling and conditional-update patterns, combining with optional chaining, and migration from older idioms.

JavaScriptintermediate
10 min read

ES2021 introduced three logical assignment operators: ||=, &&=, and ??=. They combine a logical check with an assignment in a single, short-circuiting expression. Each one conditionally assigns based on the current value of the left-hand side.

The Three Operators at a Glance

OperatorNameEquivalent ToAssigns When Left Side Is
a ||= bLogical OR assignmenta || (a = b)Falsy
a &&= bLogical AND assignmenta && (a = b)Truthy
a ??= bNullish coalescing assignmenta ?? (a = b)Null or undefined

The key difference from a = a || b is short-circuit behavior: the assignment itself is skipped if the condition is not met.

Logical OR Assignment (||=)

Assigns the right side only when the left side is falsy (false, 0, "", null, undefined, NaN):

javascriptjavascript
let title = "";
title ||= "Untitled";
console.log(title); // "Untitled" — empty string is falsy
 
let count = 0;
count ||= 10;
console.log(count); // 10 — 0 is falsy (use ??= if 0 is valid)
 
let name = "Alice";
name ||= "Default";
console.log(name); // "Alice" — truthy, assignment skipped

Practical: Filling Missing Display Values

javascriptjavascript
function renderCard(data) {
  data.title       ||= "No Title";
  data.description ||= "No description available.";
  data.thumbnail   ||= "/images/placeholder.png";
  return data;
}

Logical AND Assignment (&&=)

Assigns the right side only when the left side is truthy:

javascriptjavascript
let session = { user: "Alice", token: "abc123" };
session &&= { ...session, lastActivity: Date.now() };
console.log(session); // { user: "Alice", token: "abc123", lastActivity: ... }
 
let expired = null;
expired &&= { ...expired, lastActivity: Date.now() };
console.log(expired); // null — falsy, assignment skipped

Practical: Conditional Transformation

javascriptjavascript
let errorMessage = "Network timeout";
 
// Only transform if there's an existing message
errorMessage &&= `Error: ${errorMessage}`;
console.log(errorMessage); // "Error: Network timeout"
 
let noError = "";
noError &&= `Error: ${noError}`;
console.log(noError); // "" — falsy, assignment skipped

Practical: Sanitizing Truthy Values

javascriptjavascript
const form = {
  name: "  Alice  ",
  email: "alice@example.com",
  bio: "",
};
 
// Only trim non-empty strings
form.name  &&= form.name.trim();
form.email &&= form.email.trim();
form.bio   &&= form.bio.trim(); // skipped — "" is falsy

Nullish Coalescing Assignment (??=)

Assigns the right side only when the left side is null or undefined:

javascriptjavascript
let timeout = 0;
timeout ??= 3000;
console.log(timeout); // 0 — not nullish, kept
 
let retries = null;
retries ??= 3;
console.log(retries); // 3 — null is nullish, assigned
 
let host = undefined;
host ??= "localhost";
console.log(host); // "localhost" — undefined is nullish, assigned

Practical: Config Object Defaults

javascriptjavascript
function initConfig(config = {}) {
  config.host    ??= "localhost";
  config.port    ??= 3000;
  config.debug   ??= false;
  config.retries ??= 3;
  config.timeout ??= 5000;
  return config;
}
 
initConfig({ port: 0, debug: true });
// { port: 0, debug: true, host: "localhost", retries: 3, timeout: 5000 }
// port=0 is preserved (not nullish)

See advanced JS nullish coalescing full tutorial for the non-assignment version of ??.

Short-Circuit Behavior

The assignment itself is a side effect that is skipped when the condition is not met. This matters for setters and proxies:

javascriptjavascript
const obj = {
  _value: "existing",
  set value(v) {
    console.log("setter called with:", v);
    this._value = v;
  },
  get value() {
    return this._value;
  },
};
 
// ??= does NOT trigger the setter (left side is not nullish)
obj.value ??= "new";
// No "setter called" log — setter was never invoked
 
// Compare with: obj.value = obj.value ?? "new";
// This ALWAYS triggers the setter, even when the value doesn't change

This is the critical difference from a = a ?? b: the logical assignment version avoids unnecessary writes.

Choosing the Right Operator

GoalOperatorExample
Fill in any missing/falsy value||=title ||= "Untitled"
Update only if value exists&&=session &&= refresh(session)
Fill in only null/undefined??=config.port ??= 3000

Decision Guide

CodeCode
Is 0, "", or false a valid value you want to keep?
├── Yes  → Use ??= (only replaces null/undefined)
└── No   → Use ||= (replaces all falsy values)

Want to transform only when value is already set?
└── Yes  → Use &&=

Migration From Older Idioms

javascriptjavascript
// Old: if-based default
if (obj.x === undefined || obj.x === null) {
  obj.x = defaultValue;
}
// New:
obj.x ??= defaultValue;
 
// Old: OR-based default
obj.x = obj.x || defaultValue;
// New:
obj.x ||= defaultValue;
 
// Old: conditional update
if (obj.x) {
  obj.x = transform(obj.x);
}
// New:
obj.x &&= transform(obj.x);

Common Mistakes

Confusing ??= With ||=

javascriptjavascript
const settings = { volume: 0 };
 
settings.volume ||= 50; // 50 — BUG: 0 is valid but was overwritten
settings.volume ??= 50; // 0 — correct: 0 is preserved

Expecting ??= to Work With Deep Paths

javascriptjavascript
const obj = {};
// obj.a.b ??= "value"; // TypeError: Cannot read property 'b' of undefined
 
// You need optional chaining beforehand, but ??= does not chain:
if (obj.a != null) {
  obj.a.b ??= "value";
}
 
// Or initialize the path first:
obj.a ??= {};
obj.a.b ??= "value";
Rune AI

Rune AI

Key Insights

  • ??= is the safest default filler: It only assigns when the left side is null or undefined, preserving valid falsy values like 0 and ""
  • ||= replaces all falsy values: Use when 0, "", and false should also trigger the default
  • &&= transforms only existing values: It skips the assignment entirely when the left side is falsy
  • Short-circuit prevents unnecessary setter calls: Unlike a = a ?? b, logical assignment avoids invoking setters when no change is needed
  • Use ??= for config initialization: config.port ??= 3000 is the concise, correct way to fill missing configuration values
RunePowered by Rune AI

Frequently Asked Questions

Are logical assignment operators supported in all browsers?

Yes, in all modern browsers (Chrome 85+, Firefox 79+, Safari 14+, Edge 85+) and Node.js 15+. Transpile with Babel for older targets.

Can I use these with array elements?

Yes. `arr[0] ??= "default"` works as expected. The index expression is evaluated once.

Do these work with destructured variables?

Not directly in the destructuring pattern. Use them after destructuring: `let { x } = obj; x ??= 10;`.

Is there a performance benefit over the long form?

Yes, for objects with setters or proxies. The short-circuit avoids the setter call entirely. For plain properties, the performance difference is negligible.

Can I chain logical assignment operators?

Not meaningfully. `a ??= b ??= c` is valid but confusing. Prefer explicit separate statements for clarity.

Conclusion

Logical assignment operators condense common default-filling and conditional-update patterns into single expressions. Use ??= for filling null/undefined gaps (the most common case), ||= for replacing all falsy values, and &&= for transforming only when a value exists. Their short-circuit behavior avoids unnecessary writes, making them superior to the a = a || b idiom. For the nullish coalescing operator itself, see advanced JS nullish coalescing full tutorial. For optional chaining that pairs with these operators, see advanced JS optional chaining complete guide.