Optional chaining and nullish coalescing — what they actually solve
Both operators exist to fix a real, specific gap in JavaScript's earlier tools — ?. replaces a chain of manual null checks with one symbol, and ?? replaces || in exactly the cases where treating 0 or "" as "missing" would be a real, silent bug.
3 min read
The problem ?. (optional chaining) solves: a chain of manual existence checks
// Before optional chaining — checking EVERY level manually before accessing the next
const city = user && user.address && user.address.city;
// The SAME thing, with optional chaining
const city2 = user?.address?.city; // undefined if user OR address is null/undefined, at ANY pointAccessing a deeply nested property (user.address.city) throws if any intermediate step is null or undefined — the classic "cannot read properties of undefined" error — which is why code used to need an explicit check at every single level before descending further. ?. collapses that entire chain into one operator: user?.address?.city short-circuits to undefined the moment it hits a null/undefined at any point in the chain, without throwing, and without needing a separate && check written out for every intermediate step.
?. also works on function calls and array access, not just properties
user.greet?.(); // calls greet() ONLY if it exists — no error if greet is undefined
users?.[0]; // accesses index 0 ONLY if users itself isn't null/undefined?.() guards a function call specifically — useful for an optional callback (a config object's onComplete handler that might not be provided) without needing a separate typeof fn === "function" check beforehand. ?.[...] does the same for bracket-notation property/array access. All three forms (?., ?.(), ?.[...]) short-circuit the same way, stopping the evaluation and returning undefined the moment a null/undefined is encountered, rather than throwing.
The problem ?? (nullish coalescing) solves: ||'s real, common false-positive
function setVolume(level) {
const actualLevel = level || 50; // BUG: if level is 0 (a genuinely valid, intentional volume), this becomes 50!
}
function setVolumeFixed(level) {
const actualLevel = level ?? 50; // CORRECT: only falls back to 50 if level is null or undefined specifically
}
setVolumeFixed(0); // 0 — respects the genuinely intentional value
setVolume(0); // 50 — WRONG, silently overrides a real, valid 0|| falls back to its right-hand side for any falsy left-hand value — recall from the type-coercion lesson that 0, "", false, and NaN are all falsy, not just null/undefined — which means level || 50 silently discards a genuinely valid, intentional 0 and replaces it with 50, a real, common bug. ?? checks specifically for null or undefined — nothing else — so 0, "", and false pass through completely unchanged, only falling back when the value is genuinely absent rather than merely falsy.
When || is still the right choice, and ?? isn't a strict universal replacement
function getDisplayName(name) {
return name || "Anonymous"; // an EMPTY STRING really should fall back to "Anonymous" here
}?? isn't a strict upgrade that should replace every || — the choice depends on whether falsy-but-present values (0, "", false) should be treated as "no real value" or as "a genuine, valid value that happens to be falsy." An empty string for a display name is arguably still "no name provided," where ||'s broader falsy check is actually the intended behavior; a volume level of 0 is a real, meaningful value that ?? correctly preserves. The right operator depends on what "missing" genuinely means for that specific value.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What problem does `?.` (optional chaining) solve compared to a chain of `&&` checks?
2. Why does `level || 50` produce a real bug when level can legitimately be 0?
3. Is `??` a strict, universal replacement for `||` in every situation?