AlgoInsight

Free lesson · JavaScript Basic to Advance

Optional Chaining and Nullish Coalescing

Easy5 min read

Two Operators That Replace a Lot of Boilerplate

Modern JavaScript has two operators that together replaced a huge category of defensive coding: optional chaining (?.) for safe property access, and nullish coalescing (??) for safe defaults. Once you've used them for a few weeks, the old patterns feel painful by comparison.

const user = {
  name: "Olena",
  profile: {
    bio: "Building things"
  }
};

// Old style — explicit checks
const cityOld = user && user.profile && user.profile.address && user.profile.address.city;

// Modern — optional chaining
const cityNew = user?.profile?.address?.city;

console.log(cityOld);   // undefined
console.log(cityNew);   // undefined — same result, much cleaner

Both return undefined because address doesn't exist on user.profile. The modern version reads as a single chain instead of repeated guard checks. This Topic covers both operators with the subtleties that catch people off guard.

Optional Chaining

The ?. operator accesses a property only if the value before it is not null or undefined. If it is, the whole expression short-circuits and returns undefined.

const user = {
  name: "Olena",
  contact: {
    email: "olena@example.com"
  }
};

// Safe property access
console.log(user?.name);                // "Olena"
console.log(user?.contact?.email);      // "olena@example.com"
console.log(user?.contact?.phone);      // undefined — phone doesn't exist
console.log(user?.address?.city);       // undefined — address doesn't exist

// Without optional chaining, the next two would throw
console.log(user.missing?.something);   // undefined
// console.log(user.missing.something); // ❌ TypeError

Each ?. is a separate check. If any link in the chain is null or undefined, the whole expression short-circuits and returns undefined. No more "Cannot read property 'X' of undefined" errors at every level.

Optional chaining also works for:

Method calls. obj?.method?.() calls the method only if both obj exists and method is defined.

const user = {
  name: "Olena",
  greet() {
    return `Hi, I'm ${this.name}`;
  }
};

console.log(user.greet?.());        // "Hi, I'm Olena"
console.log(user.farewell?.());     // undefined — farewell doesn't exist
console.log(undefined?.greet?.());  // undefined — caller is undefined

Array access. arr?.[index] accesses the index only if arr exists.

const data = { items: [1, 2, 3] };

console.log(data?.items?.[0]);    // 1
console.log(data?.results?.[0]);  // undefined — results doesn't exist

The syntax is slightly different for arrays — ?.[index] instead of ?.index. The extra dot is mandatory.

Interactive animation: Three doors, one question - ?. for properties, ?.[] for array access and ?.() for method calls.

Important: The Short-Circuit Is Specifically null and undefined

A trap that catches people: optional chaining short-circuits on null and undefined only. It does not short-circuit on 0, "", false, or NaN.

const a = { value: 0 };
const b = { value: "" };
const c = { value: false };
const d = { value: null };
const e = { value: undefined };

console.log(a?.value?.toString());   // "0" — 0.toString() works
console.log(b?.value?.length);       // 0 — empty string has length 0
console.log(c?.value?.toString());   // "false" — false.toString() works
console.log(d?.value?.toString());   // undefined — short-circuits at null
console.log(e?.value?.toString());   // undefined — short-circuits at undefined

If you wrote c?.value?.toString() expecting it to be undefined because the value is falsy, you'd be surprised. Optional chaining is about existence, not truthiness. Topic 2.4 covered falsy values; optional chaining doesn't care about most of them.

Nullish Coalescing

The ?? operator returns the right-hand value only if the left-hand value is null or undefined. Otherwise it returns the left-hand value.

console.log(null ?? "default");        // "default"
console.log(undefined ?? "default");   // "default"
console.log(0 ?? "default");           // 0 — not nullish, returned as-is
console.log("" ?? "default");          // "" — not nullish
console.log(false ?? "default");       // false — not nullish

This is the same "specifically null or undefined" rule from optional chaining. 0, "", and false are valid values and pass through.

Why Not Just Use ||?

You might wonder why ?? exists when || (logical OR) seems to do something similar. They look identical for the simple case but differ in the falsy-value edge cases:

const config = {
  retries: 0,            // a valid value — zero retries
  timeout: undefined     // missing — should use default
};

// Using || — incorrect default for retries
const retriesA = config.retries || 3;   // 3 — WRONG! 0 is falsy, treated as missing
const timeoutA = config.timeout || 5000;  // 5000

// Using ?? — correct
const retriesB = config.retries ?? 3;     // 0 — correctly preserved
const timeoutB = config.timeout ?? 5000;  // 5000

console.log({ retriesA, timeoutA });
console.log({ retriesB, timeoutB });

|| treats every falsy value as "missing" and uses the default. ?? only treats null and undefined as missing. For configuration defaults, port numbers, retry counts, anywhere 0 or "" is a valid value, you need ??.

The mistake retriesA = config.retries || 3 is one of the most common bugs in JavaScript — silently turning 0 into 3 because 0 is falsy. Modern code uses ?? to avoid this entire category of bug.

graph LR
    A[?? Nullish coalescing] --> B[Returns default only<br/>for null/undefined]

    C[|| Logical OR] --> D[Returns default for<br/>any falsy value:<br/>null, undefined, 0, '', false, NaN]

    style A fill:#10b981,color:#fff
    style C fill:#f59e0b,color:#fff
This diagram compares nullish coalescing with logical OR. Nullish coalescing returns the default value only when the left side is null or undefined — every other value, including zero, empty string, and false, passes through. Logical OR returns the default for any falsy value, which means valid values like zero and empty string get incorrectly replaced. For configuration defaults and most "fill in a missing value" cases, nullish coalescing is the right choice.

Interactive animation: Falsy is not absent - 0, an empty string and false fall to the default under ||, while ?? keeps them.

Combining Optional Chaining and Nullish Coalescing

The two operators are designed to work together. Optional chaining safely reaches into possibly-missing structures, and nullish coalescing provides a default when the result is missing:

const user = {
  name: "Olena",
  preferences: {
    theme: "dark"
  }
};

// Get the user's theme, or "light" if not set
const theme1 = user?.preferences?.theme ?? "light";
console.log(theme1);   // "dark"

// Get the user's language, or "en" if not set
const lang = user?.preferences?.language ?? "en";
console.log(lang);   // "en"

// Get the user's notifications setting, or true if not set
const notifs = user?.preferences?.notifications ?? true;
console.log(notifs);   // true

This pattern — safe_access ?? default — is the modern idiom for "get this value, or use a default." Before these operators, this required multiple lines of explicit checks.

The Nullish Coalescing Assignment

A short-form operator for "assign only if currently nullish":

const settings = {
  theme: "dark",
  language: null,
  notifications: undefined
};

settings.theme ??= "light";       // "dark" already set, unchanged
settings.language ??= "en";        // null → "en"
settings.notifications ??= true;   // undefined → true
settings.fontSize ??= 14;          // doesn't exist → 14

console.log(settings);
// { theme: "dark", language: "en", notifications: true, fontSize: 14 }

This is shorthand for settings.theme = settings.theme ?? "light". Useful for initializing default values without overwriting existing ones.

There are similar shorthand operators ||= and &&= for the OR and AND cases, but they're less commonly used.

Interactive animation: The assignment forms ??=, ||= and &&= - initialise-if-absent in three characters, without overwriting a value already set.

A Word of Caution

Optional chaining and nullish coalescing are powerful, but overusing them can hide bugs. If a property should exist and doesn't, you want to know about it — not silently get undefined.

// ⚠️ Hides a real bug
function processOrder(order) {
  const total = order?.amount?.value ?? 0;
  return total * 1.08;
}

// If order.amount.value should always exist but a bug makes it missing,
// the function silently calculates tax on $0 instead of throwing

The fix is to use these operators specifically when missing values are an expected case, not as a blanket "make this not crash" tool. If a value should always exist, let the code throw when it doesn't — that's how you find bugs.

Common Pitfalls

Interactive animation: One failed check skips the whole tail - nothing to the right of the failed ?. is ever evaluated.

Warning: The ?. and ?? traps.

Confusing || with ??. || triggers on any falsy value, including 0 and "". ?? only triggers on null and undefined. For preserving valid falsy values, use ??.

Using optional chaining everywhere "to be safe". It hides bugs. If you don't expect a value to be missing, don't use ?. — let the crash signal the bug.

Forgetting that optional chaining short-circuits the whole chain. If obj?.a.b.c and obj is null, the entire expression returns undefined — a, b, and c are never evaluated. This is usually what you want, but it can hide intermediate issues.

Combining ?? with || or && without parens. JavaScript requires explicit grouping when mixing nullish coalescing with the other logical operators. a ?? b || c is a syntax error. Write (a ?? b) || c to be explicit.

Why This Matters in Interviews

Interview: "What's the difference between || and ???" is a standard modern JavaScript interview question. The expected answer covers the falsy-vs-nullish distinction with a concrete example like 0 || 3 returning 3 vs 0 ?? 3 returning 0.

Optional chaining is expected fluency. Code with verbose obj && obj.x && obj.x.y checks signals stale style.

Topic 10.7 — Interview Lens: Modern JavaScript Patterns — covers the deeper treatment.

Try It

Exercise: Refactor this verbose function using ?. and ??:

function getUserCity(user) {
  if (user && user.address && user.address.city) {
    return user.address.city;
  }
  return "Unknown";
}

function getRetryCount(config) {
  if (config && config.retries !== undefined && config.retries !== null) {
    return config.retries;
  }
  return 3;
}

The refactored versions should each be a single expression. Then test with these inputs:

  • getUserCity({ address: { city: "Redmond" } }) → "Redmond"
  • getUserCity({}) → "Unknown"
  • getRetryCount({ retries: 0 }) → 0 (NOT 3 — make sure your refactor handles this)
  • getRetryCount({}) → 3

The third case is the trap. If you reach for || instead of ??, you'll get the wrong answer.

Back to JavaScript Basic to Advance