skip to content

In JavaScript, what exactly does the optional chaining operator `?.` short-circuit, and in which situations does an expression containing `?.` still throw?

level: middleimportance: must knowfreq 72%

answer

  1. the guard aborts more than one access
  2. arguments after the guard never run
  3. only the link it is written on is safe
  4. parentheses start a fresh access
  5. a non-function value is still a TypeError

basics

~20 s

Optional chaining short-circuits the whole remaining chain: if the value before ?. is null or undefined the entire expression evaluates to undefined, and later accesses, calls and their arguments are never evaluated. It only guards the link it is written on.

solid answer

~40 s

`?.` checks whether the value on its left is `null` or `undefined`. If it is, evaluation of the *entire* chain stops and the expression yields `undefined` — later property accesses, index accesses `?.[k]`, calls `?.()` and even the arguments of those calls are skipped. If it is not nullish, evaluation continues normally. The key trap is that it guards only the link it is written on: in `a?.b.c`, if `a` is an object but `a.b` is `undefined`, the plain `.c` still throws a `TypeError`. Parentheses also end a chain, so `(a?.b).c` throws instead of short-circuiting. And `fn?.()` only tolerates a *nullish* `fn` — if `fn` holds a non-callable value like a string, you still get a `TypeError`. An undeclared identifier throws a `ReferenceError` before `?.` is ever consulted.

code

javascript · 15 lines
javascript
let calls = 0;
const arg = () => { calls++; return 1; };

const obj = null;
console.log(obj?.method(arg())); // undefined
console.log(calls);              // 0 — the argument never ran

const o = { a: {} };
try {
  o?.a.b.c;                      // guard only covers `o`
} catch (e) {
  console.log(e.constructor.name); // TypeError
}

console.log(o?.a?.b?.c);         // undefined — every link guarded

go deeper

for a junior

Know the three forms obj?.prop, obj?.[key] and fn?.(), and be able to say that the expression yields undefined when the value before the operator is null or undefined.

for a middle

Explain that the short-circuit aborts the entire chain including call arguments, and demonstrate the trap where a?.b.c still throws because only the first link is guarded.

for a senior

Show that you treat optional chaining as a modelling decision: guard values that are genuinely optional and let broken invariants throw, so failures surface where the state went wrong rather than three layers downstream.

for a principal

Be ready to argue about where absence is represented at all — whether APIs return partially populated objects that force chains of guards, and whether normalising data at the boundary removes the need for defensive access throughout the codebase.

## What the operator does `?.` is a property-access operator with a guard. `obj?.prop` evaluates `obj`; if the result is `null` or `undefined`, the expression evaluates to `undefined`; otherwise it performs the normal property access. Three forms exist: ```js obj?.prop // optional property access obj?.[expr] // optional computed access fn?.(args) // optional call ``` The last one is why the bracket and paren forms keep the dot: `obj?.[k]` and `fn?.()`, not `obj?[k]`. ## Short-circuiting is chain-wide, not link-wide The most important behaviour is that a nullish check does not just skip one access — it aborts the whole *optional chain*. Everything to the right, in the same chain, is left unevaluated: ```js const a = null; a?.b.c.d; // undefined, no error a?.b().c[expensive()]; // undefined; expensive() is never called ``` That includes call arguments. This is observable when arguments have side effects: ```js let calls = 0; const arg = () => { calls++; return 1; }; const obj = null; obj?.method(arg()); console.log(calls); // 0 ``` ## What it does NOT protect `?.` guards only the value immediately to its left. A plain `.` further down the chain is still a plain `.`: ```js const o = { a: {} }; o?.a.b.c; // TypeError: Cannot read properties of undefined (reading 'c') ``` Here `o` is not nullish, `o.a` is `{}`, `o.a.b` is `undefined`, and the unguarded `.c` throws. Guarding every uncertain link means `o?.a?.b?.c`. Writing `?.` once at the front and assuming the rest is safe is the single most common misunderstanding of the operator. ## Parentheses end the chain Short-circuiting propagates through the syntactic chain, and a parenthesised sub-expression is a new one: ```js const a = null; a?.b.c; // undefined (a?.b).c; // TypeError — a?.b is undefined, then .c is a fresh access ``` ## Optional calls tolerate nullish, not non-callable `fn?.()` means "call `fn` if it exists". If `fn` is `null` or `undefined` the result is `undefined`. If `fn` holds any other non-callable value, you get a normal `TypeError`: ```js let fn; fn?.(); // undefined fn = 'hi'; fn?.(); // TypeError: fn is not a function ``` The idiomatic use is optional callbacks: `options.onDone?.()` replaces `if (options.onDone) options.onDone()` and, importantly, calls the method with the right receiver when written as `obj.handler?.()`. ## Undeclared variables still throw `?.` is not a general "don't throw" operator. If the base identifier was never declared, the reference itself fails before any nullish check happens: ```js notDeclared?.x; // ReferenceError: notDeclared is not defined ``` For genuinely optional globals, `typeof notDeclared !== 'undefined'` is still the tool; `globalThis.notDeclared?.x` also works because the property access on `globalThis` yields `undefined` rather than throwing. ## Where it may and may not appear Optional chaining is a read operator. It cannot be the target of an assignment — `a?.b = 1` is a SyntaxError — and it cannot be used in a `new` expression (`new a?.()` is a SyntaxError). It *is* allowed with `delete`, where a nullish base makes the operation a no-op that returns `true`: ```js const a = null; delete a?.b; // true, nothing happened ``` ## Pairing with ?? Because a short-circuited chain produces `undefined`, `?.` composes naturally with the nullish coalescing operator: `user?.prefs?.theme ?? 'dark'` reads as "drill in if possible, otherwise default". This is the pattern most production code actually uses. ## Judgment: don't paper over structure Sprinkling `?.` on every access hides real defects. If a value is supposed to exist by the time this code runs, a thrown `TypeError` at the point of corruption is far more useful than an `undefined` that silently flows three layers downstream and fails somewhere unrelated. Use `?.` where absence is a genuine, expected state — optional config, optional callbacks, results that may not have loaded — and let genuinely broken invariants throw.

  • What does `(a?.b).c` do when `a` is null, and why?
    It throws a `TypeError`. The parentheses terminate the optional chain, so `a?.b` short-circuits to `undefined` and then a brand-new property access `.c` runs on that `undefined`. Short-circuiting only propagates through the syntactic chain it started in, which is why wrapping part of a chain in parentheses reintroduces the crash you were trying to avoid.
  • Can optional chaining appear on the left-hand side of an assignment?
    No — `a?.b = 1` is a SyntaxError. Optional chaining is a read-only operator. It is allowed with `delete`, where `delete a?.b` is a no-op returning `true` if `a` is nullish, but any write target must be an ordinary reference.
  • Does `?.` protect you from an undeclared variable?
    No. `notDeclared?.x` throws a `ReferenceError`, because resolving the identifier fails before the nullish check happens. Optional chaining guards against nullish *values*, not against missing *bindings*. For a possibly-absent global, use `typeof notDeclared !== 'undefined'` or read it as a property, e.g. `globalThis.notDeclared?.x`.
  • When should you deliberately not use optional chaining?
    When absence would mean the program is already broken. A `TypeError` at the point where an invariant fails is a precise, well-located signal; converting it into `undefined` lets corrupt state travel and fail somewhere unrelated. Reserve `?.` for values that are legitimately optional, such as config, callbacks, or not-yet-loaded data.

saying these in an interview costs you the question

  • Thinks one ?. at the front protects the whole chain
  • Believes ?. suppresses every error, not just nullish access
  • Says fn?.() is safe when fn holds a non-function value
  • Claims arguments are still evaluated when the chain short-circuits
  • Uses ?. everywhere to silence crashes instead of fixing state

context