Skip to main content

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 to undefined at the first null or undefined in the chain
  • ?? returns the right side only when the left is null or undefined, not for 0, "", or false
  • || returns the right side for any falsy value, including 0 and false - 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

javascript
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 it

The 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 where 0, "", or false are 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

OperatorReturns right side whenPreservesCommon use
?.Left is null/undefinedReturns undefined on stopSafe nested access
??Left is null/undefined0, "", false, NaNDefaults for optional values
||Left is any falsy valueOnly truthy valuesTruthiness-based defaults
&&Left is truthyN/AConditional 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 ??

javascript
// 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; // Correct

Mistake 2: redundant ?. after an explicit check

javascript
// 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

javascript
options.onSuccess(); // TypeError if onSuccess was not passed options.onSuccess?.(); // Returns undefined safely

Mistake 4: expecting ?? to catch all falsy values

javascript
const message = "" ?? "default"; // "" - empty string is not null/undefined const message = "" || "default"; // "default" - || does catch empty strings

Real-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

javascript
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 missing

The ?? 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

javascript
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 function

The ?.() syntax for method calls is easy to forget. It follows the same pattern as ?.prop but applies to function invocation.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?