Skip to content

Lesson 1 of 9 · 20m

Optional Chaining, Nullish Coalescing, and the End of && Guards

Read nested, maybe-missing data safely — and stop letting `||` rewrite your `0`s and empty strings.

You are three weeks into consuming a third-party webhook whose schema lives in a support ticket. Half the fields are optional, some arrive as null instead of missing, and the payload shape changes when the sender's tenant is on their legacy plan. This is exactly the situation ?. and ?? were designed for — and exactly where engineers reach for them wrong.

?. short-circuits on null and undefined only

const avatarUrl = user.profile?.avatar?.url;

If user.profile is null or undefined, the whole expression evaluates to undefined — no TypeError, no try/catch. It works for property access, dynamic keys, and calls:

const name = response.users?.[0]?.profile?.displayName;
device.connect?.();           // calls connect only if it exists

Two things ?. does not do:

  1. Check types. device.connect?.() still throws TypeError: device.connect is not a function if connect exists but is a string. Optional call means "call if present," not "call if callable."
  2. Resume protection. (a?.b).c throws if a.b is missing — the moment you close the paren, you're back to normal access.

?? vs ||: the difference that bites at 2 a.m.

|| treats '', 0, false, and NaN as missing. ?? only treats null and undefined as missing:

config.retries ?? 3;   // 0 stays 0
config.retries || 3;   // 0 becomes 3 — the bug that ships

A timeout of 0 means "no timeout" for some APIs, retries: 0 means "don't retry," "" can be a deliberate empty value. || silently rewrites all of them. ES2020 added ?? precisely because this class of bug was everywhere; it's supported in Node 14+ and every browser that matters.

You cannot mix ?? with && or || without parentheses — it's a SyntaxError, because the precedence is genuinely ambiguous:

const ok = (a ?? b) || c;   // parens required

??= assigns only when the left side is nullish — handy for defaults:

options.timeout ??= 5000;

Field notes

  • ?. masks as well as protects. If profile should always exist, sprinkling ?. hides a broken upstream contract until the undefined surfaces three layers down. Use it for genuinely optional data, not as error suppression.
  • Prefer one ?. chain over five. If you find yourself writing a?.b?.c?.d?.e, consider whether the shape should be normalized at the boundary instead.
  • user?.name ?? 'anonymous' is the idiomatic read: optional path, fallback default.

::exercise(nullish-value)

Try this

Take this defensive mess and rewrite it in two lines:

function getTheme(settings) {
  let theme = 'system';
  if (settings && settings.ui && settings.ui.theme) {
    theme = settings.ui.theme;
  }
  return theme;
}

Then break it: call getTheme({ ui: { theme: '' } }) and getTheme({ ui: null }) and make sure both do what you actually want — not what || thinks you want.