JavaScript Optional Chaining Table

PatternWhat it doesField note
obj?.propRead prop only if obj existsShort-circuits to undefined - no TypeError
obj?.method()Call only if method existsSkips when method absent - API responses
arr?.[0]Index safelyBracket form for dynamic keys and arrays
fn?.()Call only if fn is callableCallbacks that may not be passed
a?.b.cOnly a is guardedThe ? stops the WHOLE chain at a - b.c still runs if a exists
obj?.prop ?? fallbackGuard + default?? handles null AND undefined - || lets 0 and '' through
Reference: the MDN optional chaining reference. The one behavior that catches everyone: the chain short-circuits at the FIRST nullish link - a?.b.c gives undefined if a is nullish (b.c is never evaluated), but if a EXISTS and b is nullish, it throws. The guard covers exactly one link, not the whole path. Bottom line: optional chaining removes the null checks, nullish coalescing removes the default checks - they were designed as a pair, and using ?? instead of || is the difference between treating 0 as absent (||) and treating only null/undefined as absent (??). Related tools: equality table (the coercion cousin), error table (the TypeError these prevent), and fetch table for the API responses that feed the chains.

Optional chaining is the null check that reads like the sentence you'd say: obj?.prop means 'if obj exists, read prop - otherwise undefined'. The table below is the working six: property, method, bracket and call guards, plus the two behaviors that catch everyone once - the one-link short-circuit and the ?? pairing.

Bottom line: the chain short-circuits at the FIRST nullish link. a?.b.c gives undefined when a is nullish (b.c never runs), but when a exists and b is nullish it THROWS - the guard covers exactly one link, not the whole path. Chain guards where nullishness actually appears (API responses: data?.user?.name), not everywhere.

The honest part: optional chaining removes the null checks and nullish coalescing removes the default checks - they were designed as a pair. a?.b ?? 'default' treats null and undefined as absent while keeping 0 and empty string (which || would swallow). Migrating || to ?? is the quiet bug-fix of modern codebases: form inputs with 0 stopped resetting to defaults.

How to use

  1. Guard where nullishness actually arrives: API responses, optional props, user-provided objects - not on values you constructed.
  2. Combine with ?? for defaults: data?.count ?? 0 reads 'count, or 0 when absent' - including when count is legitimately 0.
  3. Use fn?.() for callbacks that may not be passed - the guard tests callability, not just existence.

Frequently asked questions

Why does a?.b.c still throw when b is missing?

Because optional chaining guards ONE link: a?.b short-circuits if a is nullish, but then .c is a plain property access on the result. When a exists but b is undefined, reading c of undefined throws. The fix is chaining the guards through the whole risky path: a?.b?.c short-circuits at whichever link is nullish. Reading it as 'the guard covers the link it's attached to' predicts the behavior in every case.

How is ?. different from || when setting defaults?

|| treats ALL falsy values as absent: 0, '' and false fall through to the default. ?? (nullish coalescing) only falls through for null and undefined. The difference is data, not style: a count of 0, a volume of '' and an enabled=false are real values that || would silently replace with the default. The modern pairing is ?. for safe access and ?? for safe defaults - they were standardized together for exactly this.

When is fn?.() the right call?

When a callback is optional in the contract: hooks, event handlers, or API options where the caller may omit the function. fn?.() calls it only when it exists and is callable, returning undefined otherwise - one character-ish of code replacing if (typeof fn === 'function'). The caveat: it guards callability, not safety - the called function can still throw, so validation belongs inside the contract.

Does optional chaining work on the left side of an assignment?

No - obj?.prop = value is a SyntaxError by design: 'assign to nothing' has no meaning, and the committee chose loud failure over a silent no-op. You can still write THROUGH a guarded path into an existing container: obj?.deep[prop] = value works when obj exists. The rule: reads chain freely, writes need the container to genuinely exist - use a default object first if the target might be absent.

Related tools