Optional Chaining and Nullish Coalescing: The Right Way to Handle Missing Data
How ?. and ?? work, why ?? is not the same as ||, and the short-circuit behaviour that surprises people. With practical patterns for API data.
Table of contents
- Optional chaining short-circuits the whole chain
- Nullish coalescing is not the same as ||
- Assignment forms
- A practical pattern for API responses
- Frequently asked questions
- Does optional chaining hurt performance?
- Should I use ?. everywhere to be safe?
- Can I use ?. with delete?
- What about the pipeline of ?. with function calls that might not be functions?
- Related reading
- References
Two operators replaced a decade of defensive boilerplate. Both are simple, and both have one behaviour that catches people out.
Optional chaining short-circuits the whole chain#
?. returns undefined instead of throwing when the value to its left is null or undefined.
const city = user?.address?.city; // undefined, never a TypeErrorThe important detail is that it short-circuits the entire rest of the expression, not just the next access:
const value = user?.address.city.postcode;
// If user is null, the whole thing is undefined.
// If user exists but address is null, this STILL throws.So ?. is not a blanket safety net — it guards exactly the link it is attached to. Put it after every value that can genuinely be missing, and nowhere else. An unnecessary ?. hides a real bug: if address should always exist, you want the error.
It works on calls and indexes too:
callback?.(); // call only if callback exists
config?.['key']; // dynamic property
items?.[0]?.name; // array elementNullish coalescing is not the same as ||#
?? returns the right-hand side only when the left is null or undefined. || returns it for any falsy value.
0 ?? 10; // 0
0 || 10; // 10 ← usually a bug
'' ?? 'default'; // ''
'' || 'default'; // 'default'
false ?? true; // false
false || true; // trueThis matters enormously for configuration and form data, where 0, '' and false are legitimate values:
// Bug: a user who sets quantity to 0 gets 1
const quantity = input.quantity || 1;
// Correct: only an absent value falls back
const quantity = input.quantity ?? 1;Assignment forms#
config.retries ??= 3; // assign only if null/undefined
cache.items ||= []; // assign if falsy
flags.debug &&= isDev; // assign only if currently truthy??= is the one to reach for when initialising optional config, for the same reason as above.
A practical pattern for API responses#
The combination is at its best when reading data you do not control:
function normaliseUser(raw) {
return {
id: raw?.id ?? null,
name: raw?.profile?.displayName ?? 'Anonymous',
// ?? not || — 0 followers is a real value
followers: raw?.stats?.followers ?? 0,
// ?. on the method, ?? on the result
tags: raw?.tags?.map((t) => t.name) ?? [],
isVerified: raw?.flags?.verified ?? false,
};
}Every fallback here is deliberate, and none of them silently converts a meaningful zero or empty string into a default.
Frequently asked questions#
Does optional chaining hurt performance?#
No measurably. It compiles to a null check. The cost is a comparison, which is unmeasurable next to a property lookup.
Should I use ?. everywhere to be safe?#
No. It suppresses errors, and an error is useful information when a value should never be missing. Use it where absence is a legitimate state; let the exception happen where absence is a bug.
Can I use ?. with delete?#
Yes: delete user?.profile.temp is valid and does nothing if user is nullish.
What about the pipeline of ?. with function calls that might not be functions?#
?.() only guards against null/undefined. If the value exists but is a string, you still get "is not a function". Check with typeof callback === 'function' when the type is genuinely unknown.
Related reading#
- JavaScript Equality Compared — the falsy/nullish distinction in depth
- TypeScript Strict Mode —
strictNullChecksmakes the compiler tell you where?.is needed