Optional chaining (?.) and nullish coalescing (??) in JavaScript
Optional chaining (?.) and nullish coalescing (??) are two ES2020 operators that solve different problems: ?. prevents crashes on missing properties, ?? provides defaults without replacing valid falsy values like 0, "", or false.
Theory
TL;DR
?.short-circuits toundefinedat the firstnullorundefinedin the chain??returns the right side only when the left isnullorundefined, not for0,"", orfalse||returns the right side for any falsy value, including0andfalse- that is often a bug- Use them together:
user?.profile?.name ?? "Guest"- safe access with a fallback - Decision rule: chain with
?.first, then add a default with??at the end
Quick example
const user = { name: "Alice" };
// ?. stops at the first null/undefined
console.log(user?.address?.city); // undefined (no error)
console.log(user?.address?.city ?? "NYC"); // "NYC" (fallback applied)
// ?? vs || - the key difference
const count = 0;
console.log(count ?? 10); // 0 - zero is valid data, ?? leaves it
console.log(count || 10); // 10 - || treats 0 as falsy and replaces itThe last two lines are the most common source of bugs when working with these operators. If count can legitimately be 0, always use ??.
Key difference
?. is about defensive access. ?? is about value defaulting. They solve different problems and work together naturally. ?. prevents TypeError: Cannot read properties of undefined. ?? makes sure a fallback does not accidentally replace 0 or false, which || would silently overwrite.
When to use
?.: accessing nested objects from API responses, calling optional callbacks, reading config that might not be set??: providing defaults for settings where0,"", orfalseare valid values?.and??together:response?.data?.count ?? 0- safe traversal with a meaningful fallback- Stick with
||for truthiness checks: toggling UI states or treating empty strings as "missing" still suits||
Comparison table
| Operator | Returns right side when | Preserves | Common use |
|---|---|---|---|
?. | Left is null/undefined | Returns undefined on stop | Safe nested access |
?? | Left is null/undefined | 0, "", false, NaN | Defaults for optional values |
|| | Left is any falsy value | Only truthy values | Truthiness-based defaults |
&& | Left is truthy | N/A | Conditional execution |
How it works internally
Both operators are part of ES2020 and are natively supported in Node.js 14+ and all modern browsers. When the engine hits obj?.prop, it checks whether obj is null or undefined. If yes, it returns undefined immediately without evaluating the rest. The chain a?.b?.c?.d stops at the first nullish value, so nothing after it runs. Babel transpiles both operators for older targets using equivalent conditional checks.
Common mistakes
Mistake 1: using || when you need ??
// Bug: a timeout of 0 means "no delay", but || replaces it
const timeout = settings.timeout || 5000; // 5000 even when timeout is 0
const timeout = settings.timeout ?? 5000; // CorrectMistake 2: redundant ?. after an explicit check
// Unnecessary - address is already confirmed
if (user && user.address) {
const city = user.address?.city;
}
// Simpler
const city = user?.address?.city;Mistake 3: forgetting ?. on optional method calls
options.onSuccess(); // TypeError if onSuccess was not passed
options.onSuccess?.(); // Returns undefined safelyMistake 4: expecting ?? to catch all falsy values
const message = "" ?? "default"; // "" - empty string is not null/undefined
const message = "" || "default"; // "default" - || does catch empty stringsReal-world usage
- React:
props?.user?.avatar ?? "/default.png"- optional props in components - Express.js:
req?.query?.search ?? ""- reading optional query params - Redux selectors:
state?.auth?.user?.role ?? "guest"- nested store access - API responses:
response?.data?.items?.[0]?.id ?? null- traversing nullable fields - Optional callbacks:
options.onSuccess?.()- safe calls when callback might not be passed
Follow-up questions
Q: What is the difference between obj?.prop and obj && obj.prop?
A: Both prevent errors, but the return value differs. obj && obj.prop returns the falsy value itself if obj is 0 or "". ?. always returns undefined when the chain stops. ?. also signals intent more clearly.
Q: Why does 0 ?? 10 return 0 but 0 || 10 returns 10?
A: ?? checks specifically for null or undefined, not for falsiness. Zero is falsy but not nullish, so ?? treats it as valid data and leaves it unchanged.
Q: Can you use optional chaining in destructuring?
A: Not directly in the destructuring syntax. The workaround is const { name } = user?.profile ?? {}. Without ?? {}, destructuring would throw if user?.profile returns undefined.
Q: What happens with multiple ?. in a chain like a?.b?.c?.d?
A: The whole chain short-circuits at the first null or undefined and returns undefined. Each ?. is independent. If a.b is null, neither c nor d is evaluated.
Q (senior): In a loop processing millions of objects, which would you prefer: obj?.prop ?? default or obj && obj.prop || default?
A: Modern engines optimize both similarly, and ?. is a single operator vs two separate operations. The stronger reason to prefer ?. is clarity: it makes null-safe access obvious at a glance. Profile first before changing anything in a hot path.
Examples
Basic: API response in a React component
function UserProfile({ apiResponse }) {
const userName = apiResponse?.data?.user?.name ?? "Anonymous";
const avatarUrl = apiResponse?.data?.user?.avatar ?? "/default-avatar.png";
const isAdmin = apiResponse?.data?.user?.permissions?.isAdmin ?? false;
return (
<div>
<h1>Hello, {userName}</h1>
<img src={avatarUrl} alt={userName} />
{isAdmin && <span>Admin</span>}
</div>
);
}
// Works even when apiResponse is null or data is partially missingThe ?? on isAdmin matters here. isAdmin || false would also work, but ?? makes the intent clear: only substitute false when the value was never set.
Intermediate: optional method calls and array access
const response = {
items: [
{ id: 1, process: () => "processed" },
{ id: 2 } // No process method
]
};
console.log(response.items?.[0]?.process?.()); // "processed"
console.log(response.items?.[1]?.process?.()); // undefined - no error
console.log(response.items?.[5]?.process?.()); // undefined - index out of bounds
const result = response.items?.[1]?.process?.() ?? "default processing";
console.log(result); // "default processing"
// Without optional chaining - crashes
response.items[1].process(); // TypeError: process is not a functionThe ?.() syntax for method calls is easy to forget. It follows the same pattern as ?.prop but applies to function invocation.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.