Optional chaining and nullish coalescing
Three lessons have now produced the same error:
TypeError: Cannot read properties of undefined (reading 'city')
It is the most common runtime error in JavaScript, and since 2020 there has been a one-character fix. This lesson is short because the feature is small — but it appears in almost every file you will write.
The problem, stated plainly
const order = { customer: 'Priya' };
console.log(order.address.city);
TypeError: Cannot read properties of undefined (reading 'city')
order.address is undefined. Reading .city from undefined throws.
This happens constantly with real data — an optional field, a customer with no
address yet, an API that omits keys rather than sending nulls. The old defence
was a chain of &&, from module 2:
const city = order && order.address && order.address.city;
That works and it is horrible. Three mentions of order to read one value, and
it gets worse with depth.
Optional chaining
console.log(order?.address?.city);
undefined
?. means: if the thing on the left is null or undefined, stop and give
undefined. Otherwise carry on. No error.
Crucially it short-circuits — once it bails out, the rest of the chain is never evaluated:
console.log(order?.address?.city.toUpperCase());
undefined
That did not throw, even though the last .toUpperCase() has no ?. in front
of it. The chain stopped at address and abandoned everything after.
Where the question mark goes
Put ?. after the thing that might be missing, not after the thing you are
reading.
order.address?.city
says "order definitely exists; address might not". That is usually what you
mean, and it is better than order?.address?.city — because if order is
genuinely never missing, you want an error when it is, rather than a silent
undefined hiding a real bug.
Do not sprinkle ?. everywhere. It is a statement that a value is legitimately
optional. Used on things that should always exist, it converts loud failures into
quiet wrong answers, which is a bad trade.
The other two forms
For array indexes and dynamic keys, ?.[:
const orders = null;
console.log(orders?.[0]);
undefined
For functions that may not exist, ?.(:
const onDelivered = undefined;
console.log(onDelivered?.());
undefined
That one is useful for optional callbacks — options.onSuccess?.(data) calls the
handler if one was given and does nothing if not.
| Form | Use |
|---|---|
a?.b |
Property |
a?.[key] |
Index or computed key |
a?.() |
Call, if it is a function |
Nullish coalescing
?. gives you undefined when something is missing. Usually you want a
fallback, and module 2 showed why || is the wrong tool:
console.log(0 || 5);
console.log(0 ?? 5);
5
0
|| falls back on any falsy value. ?? falls back only on null and
undefined.
console.log('' || 'none');
console.log('' ?? 'none');
none
The second printed an empty line — '' is falsy but not nullish, so ?? kept
it.
That difference is the whole point. A plate count of 0, an empty delivery note,
a false setting — all legitimate values that || would throw away.
| Expression | || |
?? |
|---|---|---|
0 |
falls back | keeps 0 |
'' |
falls back | keeps '' |
false |
falls back | keeps false |
NaN |
falls back | keeps NaN |
null |
falls back | falls back |
undefined |
falls back | falls back |
Use ?? for defaults. Use || only when you genuinely mean "any falsy value
should be replaced" — which is rarer than people write it.
The two together
This pairing is the point of the lesson:
const city = order?.address?.city ?? 'No city recorded';
console.log(city);
No city recorded
"Dig down safely; if anything is missing, use this instead." One line, no
repetition, and correct when city is legitimately an empty string.
A real example, reading an API response:
function describe(response) {
const customer = response?.data?.customer?.name ?? 'Unknown';
const plates = response?.data?.order?.plates ?? 0;
return `${customer}: ${plates} plates`;
}
console.log(describe({ data: { order: { plates: 0 } } }));
console.log(describe(null));
Unknown: 0 plates
Unknown: 0 plates
Note the first one keeps the genuine 0 — with || it would have said 0 as
well, since the fallback is also 0, but change the fallback to 1 and the
difference becomes a wrong bill.
One syntax note
You cannot mix ?? with && or || without brackets:
const value = a ?? b || c;
SyntaxError: Unexpected token '||'
The language refuses rather than guessing at precedence. Add brackets to say what
you mean: (a ?? b) || c.
Check your work
order?.address?.city on { customer: 'Priya' } is undefined, with no
error.
order?.address?.city.toUpperCase() also gives undefined, not an error,
because ?. short-circuits the whole rest of the chain once it bails.
order.address?.city is the better form when order itself should always
exist — you keep a real error for a real bug and only guard the part that is
genuinely optional.
orders?.[0] on null is undefined; onDelivered?.() on undefined is
undefined.
0 || 5 is 5; 0 ?? 5 is 0. || replaces any falsy value; ??
replaces only null and undefined.
'' ?? 'none' is '' — an empty string survives ?? and would not survive
||.
a ?? b || c is a SyntaxError. Brackets are required — and they are not a
formality. With a = 0, b = 'b', c = 'c':
(a ?? b) || c // 'c' — ?? keeps the 0, then || throws it away
a ?? (b || c) // 0 — ?? keeps the 0 and stops
Two different answers from the same three values, which is exactly why the language refuses to guess.
order?.address?.city ?? 'No city recorded' is the idiomatic combination:
navigate safely, then supply a default.
getSetting cannot use ?., because the path is a string decided at run
time and ?. is syntax you write in advance. You walk it instead:
function getSetting(config, path, fallback) {
let current = config;
for (const part of path.split('.')) {
if (current == null) return fallback;
current = current[part];
}
return current ?? fallback;
}
current == null is the one sanctioned == from module 2, catching null and
undefined together. The final ?? fallback rather than || fallback is what
keeps a genuine stored 0 or '' — with ||, a configured start time of 0
would silently become the fallback.
Practice
- Cause the error first. Read
order.address.cityon an order with no address. Then fix it with?.and confirm you getundefined. - Write the old
&&chain for the same thing and compare the two for readability. - Prove short-circuiting: chain a method call on the end with no
?.in front of it and confirm it still does not throw. - Try
order?.address?.cityandorder.address?.cityon an order that exists and on one that isnull. Decide which form you would use in real code, and why. - Use
?.[0]on an array that isnull, and?.()on a callback that was not supplied. - Run every row of the
||versus??table yourself. - Find the bug.
const plates = order.plates || 1— giveorder.platesa real value of0and watch a cancelled order become one plate. Fix it. - Write
a ?? b || cand read theSyntaxError. Then, witha = 0,b = 'b'andc = 'c', evaluate(a ?? b) || canda ?? (b || c)and explain why the brackets change the answer. - Harder. Write
getSetting(config, path, fallback)wherepathis a string like'delivery.window.start', returning the value at that path or the fallback if any part is missing. You cannot use?.for a path you do not know at the time of writing — so work out what you use instead, and make sure a stored value of0or''is returned rather than replaced by the fallback.
Next: Map and Set — for the jobs where a plain object is the wrong shape,
including the string-keys problem from the objects lesson.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship