Module 6 · Modern JavaScript & Tooling Concepts ⏱ 19 min

Optional Chaining & Nullish Coalescing

By the end of this lesson you will be able to:
  • Safely access nested properties with optional chaining instead of long && ladders
  • Provide defaults only for null or undefined with nullish coalescing
  • Combine ?. and ?? to handle missing data gracefully in nested objects

Modern web applications are built from nested objects. A user has a profile, the profile has an address, and the address has a city. If any link in that chain is missing — perhaps the user has not filled out their address yet — a direct access such as user.profile.address.city throws a TypeError: Cannot read property 'city' of undefined. That single missing field crashes the entire function and can bring down a UI component that should have simply rendered an empty string.

The traditional fix is defensive coding with the logical AND operator: user && user.profile && user.profile.address && user.profile.address.city. It works, but it is repetitive, noisy, and easy to mistype. Every extra level adds another && and another chance to forget a property name. As data structures grow deeper, the ladder grows longer and the code becomes harder to read. Optional chaining was added to the language specifically to make this pattern disappear without sacrificing safety.

flowchart LR
  A["user.profile.address.city"] --> B{"missing address?"}
  B -- Yes --> C["TypeError"]
  B -- No --> D["returns city"]
  E["user?.profile?.address?.city"] --> F{"missing address?"}
  F -- Yes --> G["undefined"]
  F -- No --> D
  style C fill:#b91c1c,color:#fff
  style G fill:#1e7f3d,color:#fff
Direct access throws on a missing link; optional chaining returns undefined.

Optional chaining

Optional chaining uses the ?. operator. If the value before the question mark is null or undefined, the whole expression short-circuits and returns undefined instead of throwing. If the value exists, the access proceeds normally. The operator is concise but not magical: it only checks the immediate left-hand side, not every possible error condition. If user is an object but user.profile is a number, user.profile?.address will still throw when it tries to access .address on a number.

const city = user?.profile?.address?.city;

That one line replaces the entire && ladder. It reads left to right: try user, then profile, then address, then city. Stop at the first missing link and hand back undefined. The operator also works with bracket notation — obj?.[key] — and with method calls: obj.method?.(). If method is missing, the call evaluates to undefined rather than crashing. You can chain ?. as deep as your data structure requires, though deeply nested objects are sometimes a sign that the data model should be flattened.

Optional chaining returns undefined at the first missing link instead of throwing.
const user = {
  name: 'Ada',
  profile: {
    age: 30
  }
};
console.log(user?.profile?.age);
console.log(user?.address?.city);
console.log(user?.greet?.());

Nullish coalescing

Providing a default value is the natural next step after optional chaining. The older approach is the logical OR operator: value || 'default'. It replaces any falsy value — 0, empty string "", false, NaN, null, and undefined — with the fallback. That behavior is often too aggressive. If a user's score is legitimately 0, the || operator replaces it with a default as if the score were missing. The same problem strikes empty strings that are intentionally blank and false booleans that are intentionally off.

Nullish coalescing, written ??, only falls back when the left side is null or undefined. It treats 0, "", and false as real values and keeps them. This makes ?? the correct choice for almost every default you will write, because it respects data that happens to be falsy. Reach for || only when you genuinely want to treat zero or an empty string as missing, which is rare in practice.

flowchart TD
  Z["value is 0 or empty string"] --> OR["|| replaces it"]
  Z --> NC["?? keeps it"]
  OR --> D["fallback fires"]
  NC --> K["original value kept"]
  style OR fill:#b91c1c,color:#fff
  style NC fill:#1e7f3d,color:#fff
Logical OR replaces 0 and empty string; nullish coalescing preserves them.
?? preserves 0 and empty string; || discards them.
const score = 0;
const name = '';
console.log(score || 100);
console.log(score ?? 100);
console.log(name || 'Anonymous');
console.log(name ?? 'Anonymous');

Combining ?. and ??

Optional chaining and nullish coalescing are designed to work together. Use ?. to reach into a nested object, and ?? to provide a fallback if the result is missing. The combination is so common that you will see it in almost every modern JavaScript codebase:

const city = user?.address?.city ?? 'Unknown';

The ?. operator has higher precedence than ??, so the chain is evaluated first and the fallback applies only to the final result. You do not need extra parentheses for this common case, but you should remember the rule for more complex expressions. You can also use ?? with simple variables: const port = config.port ?? 3000. If config.port is 0, it stays 0; if it is undefined, it becomes 3000. That distinction is exactly why ?? exists and why it has largely replaced || for default values in modern code.

flowchart LR
  A["user?.address?.city"] --> B{"nullish?"}
  B -- Yes --> C["?? Unknown"]
  B -- No --> D["returns city"]
  style C fill:#3776ab,color:#fff
  style D fill:#1e7f3d,color:#fff
Reach safely with ?., then fall back with ?? only when the result is missing.
Chain optional chaining with nullish coalescing for safe nested defaults.
const user = {
  name: 'Ada',
  settings: {
    theme: 'dark'
  }
};
console.log(user?.settings?.theme ?? 'light');
console.log(user?.profile?.bio ?? 'No bio available');

Real-world patterns

You will see ?. and ?? in almost every API response handler. Fetching data from a server often returns nested objects where some fields are absent depending on the user's permissions or the record's completeness. Instead of wrapping every access in a try-catch block, modern code uses optional chaining to drill safely and nullish coalescing to provide sensible defaults. Configuration objects also benefit: const timeout = config?.retry?.timeout ?? 5000 reads like a sentence and fails safely if the configuration file is partial. The pattern is so effective that lint rules now discourage long && chains in favor of ?..

When optional chaining is not enough

Optional chaining is a safety rail, not a substitute for validation. If a property is expected to be an object but arrives as a string, ?. will not help — it only guards against null and undefined. You still need schema validation or type checking when the shape of your data is unpredictable. Similarly, do not use ?. to hide errors that should be fixed at the source. A missing property that represents a bug is better caught early with an explicit check or a thrown error, rather than silently returning undefined and propagating the failure downstream. Use ?. for optional data, not for broken data.

Exercise

What does this print? Optional chaining stops at the first missing property.

const data = { a: { b: 2 } };
console.log(data?.a?.b);
console.log(data?.x?.y);
Exercise

Write getLength(str) that returns str.length if str is not null or undefined, otherwise returns 0. Use optional chaining.

function getLength(str) {
  // return str.length safely, or 0
}
Exercise

This function is meant to return a user's display name, falling back to 'Guest' only when the name is missing. But it uses ||, so a user with the name '' or 0 gets replaced too. Fix it to use nullish coalescing so that an empty string or 0 is preserved.

function displayName(name) {
  return name || 'Guest';
}
Exercise

What does this print? Remember that ?? only falls back for null or undefined, not for other falsy values.

const count = 0;
const label = null;
console.log(count ?? 10);
console.log(label ?? 'none');
Exercise

Write deepGet(obj, path, fallback) that safely reads a nested property from obj following the path array of keys. Use optional chaining between each step. If any key is missing, return fallback. For example, deepGet({a:{b:2}}, ['a','b'], 0) returns 2, and deepGet({}, ['a','b'], 0) returns 0.

function deepGet(obj, path, fallback) {
  // safely navigate obj using path keys
}

Recap

  • Optional chaining (?.) returns undefined when a property or method is missing, instead of throwing.
  • It short-circuits: the chain stops at the first null or undefined link.
  • Nullish coalescing (??) provides a fallback only for null and undefined, preserving 0, "", and false.
  • ?? has lower precedence than || and &&; mix them with explicit parentheses.
  • ?. and ?? combine naturally: obj?.prop ?? fallback.
  • Optional chaining cannot be used on the left side of an assignment.
  • These two operators remove most of the defensive && and || boilerplate from nested data access.
  • Use ?. for optional fields, not as a band-aid for broken data shapes.

Next you will meet iterators and generators — the protocols that let your own objects work with for-of loops and spread syntax.

Checkpoint quiz

What does user?.profile?.name return if user is null?

Which value would cause value ?? 'default' to return 'default'?

Go deeper — technical resources