Modern web applications are built from nested objects. A user has a profile, the profile has an address, and the address has a city. If any link in that chain is missing — perhaps the user has not filled out their address yet — a direct access such as user.profile.address.city throws a TypeError: Cannot read property 'city' of undefined. That single missing field crashes the entire function and can bring down a UI component that should have simply rendered an empty string.
The traditional fix is defensive coding with the logical AND operator: user && user.profile && user.profile.address && user.profile.address.city. It works, but it is repetitive, noisy, and easy to mistype. Every extra level adds another && and another chance to forget a property name. As data structures grow deeper, the ladder grows longer and the code becomes harder to read. Optional chaining was added to the language specifically to make this pattern disappear without sacrificing safety.
flowchart LR
A["user.profile.address.city"] --> B{"missing address?"}
B -- Yes --> C["TypeError"]
B -- No --> D["returns city"]
E["user?.profile?.address?.city"] --> F{"missing address?"}
F -- Yes --> G["undefined"]
F -- No --> D
style C fill:#b91c1c,color:#fff
style G fill:#1e7f3d,color:#fff
Optional chaining
Optional chaining uses the ?. operator. If the value before the question mark is null or undefined, the whole expression short-circuits and returns undefined instead of throwing. If the value exists, the access proceeds normally. The operator is concise but not magical: it only checks the immediate left-hand side, not every possible error condition. If user is an object but user.profile is a number, user.profile?.address will still throw when it tries to access .address on a number.
const city = user?.profile?.address?.city;
That one line replaces the entire && ladder. It reads left to right: try user, then profile, then address, then city. Stop at the first missing link and hand back undefined. The operator also works with bracket notation — obj?.[key] — and with method calls: obj.method?.(). If method is missing, the call evaluates to undefined rather than crashing. You can chain ?. as deep as your data structure requires, though deeply nested objects are sometimes a sign that the data model should be flattened.
const user = {
name: 'Ada',
profile: {
age: 30
}
};
console.log(user?.profile?.age);
console.log(user?.address?.city);
console.log(user?.greet?.());
Nullish coalescing
Providing a default value is the natural next step after optional chaining. The older approach is the logical OR operator: value || 'default'. It replaces any falsy value — 0, empty string "", false, NaN, null, and undefined — with the fallback. That behavior is often too aggressive. If a user's score is legitimately 0, the || operator replaces it with a default as if the score were missing. The same problem strikes empty strings that are intentionally blank and false booleans that are intentionally off.
Nullish coalescing, written ??, only falls back when the left side is null or undefined. It treats 0, "", and false as real values and keeps them. This makes ?? the correct choice for almost every default you will write, because it respects data that happens to be falsy. Reach for || only when you genuinely want to treat zero or an empty string as missing, which is rare in practice.
flowchart TD Z["value is 0 or empty string"] --> OR["|| replaces it"] Z --> NC["?? keeps it"] OR --> D["fallback fires"] NC --> K["original value kept"] style OR fill:#b91c1c,color:#fff style NC fill:#1e7f3d,color:#fff
const score = 0; const name = ''; console.log(score || 100); console.log(score ?? 100); console.log(name || 'Anonymous'); console.log(name ?? 'Anonymous');
Combining ?. and ??
Optional chaining and nullish coalescing are designed to work together. Use ?. to reach into a nested object, and ?? to provide a fallback if the result is missing. The combination is so common that you will see it in almost every modern JavaScript codebase:
const city = user?.address?.city ?? 'Unknown';
The ?. operator has higher precedence than ??, so the chain is evaluated first and the fallback applies only to the final result. You do not need extra parentheses for this common case, but you should remember the rule for more complex expressions. You can also use ?? with simple variables: const port = config.port ?? 3000. If config.port is 0, it stays 0; if it is undefined, it becomes 3000. That distinction is exactly why ?? exists and why it has largely replaced || for default values in modern code.
flowchart LR
A["user?.address?.city"] --> B{"nullish?"}
B -- Yes --> C["?? Unknown"]
B -- No --> D["returns city"]
style C fill:#3776ab,color:#fff
style D fill:#1e7f3d,color:#fff
const user = {
name: 'Ada',
settings: {
theme: 'dark'
}
};
console.log(user?.settings?.theme ?? 'light');
console.log(user?.profile?.bio ?? 'No bio available');
Real-world patterns
You will see ?. and ?? in almost every API response handler. Fetching data from a server often returns nested objects where some fields are absent depending on the user's permissions or the record's completeness. Instead of wrapping every access in a try-catch block, modern code uses optional chaining to drill safely and nullish coalescing to provide sensible defaults. Configuration objects also benefit: const timeout = config?.retry?.timeout ?? 5000 reads like a sentence and fails safely if the configuration file is partial. The pattern is so effective that lint rules now discourage long && chains in favor of ?..
When optional chaining is not enough
Optional chaining is a safety rail, not a substitute for validation. If a property is expected to be an object but arrives as a string, ?. will not help — it only guards against null and undefined. You still need schema validation or type checking when the shape of your data is unpredictable. Similarly, do not use ?. to hide errors that should be fixed at the source. A missing property that represents a bug is better caught early with an explicit check or a thrown error, rather than silently returning undefined and propagating the failure downstream. Use ?. for optional data, not for broken data.
What does this print? Optional chaining stops at the first missing property.
const data = { a: { b: 2 } };
console.log(data?.a?.b);
console.log(data?.x?.y);
data.a exists, so data?.a?.b reaches the value 2.
data.x is undefined, so data?.x?.y short-circuits to undefined.
Write getLength(str) that returns str.length if str is not null or undefined, otherwise returns 0. Use optional chaining.
function getLength(str) {
// return str.length safely, or 0
}
Use str?.length to safely read the property.
Use ?? 0 to provide the fallback only for null or undefined.
function getLength(str) {
return str?.length ?? 0;
}
This function is meant to return a user's display name, falling back to 'Guest' only when the name is missing. But it uses ||, so a user with the name '' or 0 gets replaced too. Fix it to use nullish coalescing so that an empty string or 0 is preserved.
function displayName(name) {
return name || 'Guest';
}
Replace || with ?? so only null and undefined trigger the fallback.
?? preserves 0 and empty string; || does not.
function displayName(name) {
return name ?? 'Guest';
}
What does this print? Remember that ?? only falls back for null or undefined, not for other falsy values.
const count = 0; const label = null; console.log(count ?? 10); console.log(label ?? 'none');
0 is not nullish, so ?? keeps it.
null is nullish, so ?? falls back to 'none'.
Write deepGet(obj, path, fallback) that safely reads a nested property from obj following the path array of keys. Use optional chaining between each step. If any key is missing, return fallback. For example, deepGet({a:{b:2}}, ['a','b'], 0) returns 2, and deepGet({}, ['a','b'], 0) returns 0.
function deepGet(obj, path, fallback) {
// safely navigate obj using path keys
}
Loop through path and update current = current?.[key] each time.
After the loop, return current ?? fallback.
function deepGet(obj, path, fallback) {
let current = obj;
for (const key of path) {
current = current?.[key];
}
return current ?? fallback;
}
Recap
- Optional chaining (
?.) returnsundefinedwhen a property or method is missing, instead of throwing. - It short-circuits: the chain stops at the first
nullorundefinedlink. - Nullish coalescing (
??) provides a fallback only fornullandundefined, preserving0,"", andfalse. ??has lower precedence than||and&&; mix them with explicit parentheses.?.and??combine naturally:obj?.prop ?? fallback.- Optional chaining cannot be used on the left side of an assignment.
- These two operators remove most of the defensive
&&and||boilerplate from nested data access. - Use
?.for optional fields, not as a band-aid for broken data shapes.
Next you will meet iterators and generators — the protocols that let your own objects work with for-of loops and spread syntax.