JAVASCRIPT / OPERATORS
Optional chaining for safe property access
Read deeply nested values and call maybe-missing methods with ?., knowing exactly which links short-circuit to undefined and which still throw.
What you will learn
- Guard one link with a?.b so a missing object yields undefined instead of a TypeError
- Use ?.[key] for computed keys and ?.() for methods that may not exist
- Predict short-circuiting: only null and undefined trigger it, never 0, '' or false
- Put ?. on the link that can actually be missing, not just at the front of the chain
Understanding Optional chaining for safe property access
The ?. operator is a guard on the value immediately to its left. In user.address?.city, JavaScript evaluates user.address first; if that is null or undefined the whole expression is undefined, and if it is anything else the property read happens normally. Without the guard, a plain dot on a nullish value throws TypeError: Cannot read properties of undefined and aborts the surrounding function. Note that a null left side still produces undefined, not null, because the operator collapses both nullish cases into one result.
The word chaining is literal: once a ?. short-circuits, the entire rest of the chain is abandoned, not just the next property. a?.b.c.d never touches .c or .d when a is nullish, and a?.b(expensive()) never even evaluates expensive(). The token is always the two characters ?., so dynamic keys are written obj?.[key] and calls fn?.(arg); a bare obj?[key] would be ambiguous with the conditional operator. Parentheses end a chain, which is why (a?.b).c throws where a?.b.c does not.
Because the only trigger is null or undefined, values like 0, '', false and NaN pass straight through: ''?.length is 0, not undefined. So treat each ?. as a claim that this particular link is optional by design; on links that should always exist, keep the plain dot, since the TypeError points at the real bug instead of letting undefined drift silently into arithmetic or the DOM. Also, ?. can only read, never write: user?.name = 'Ada' is a SyntaxError.
const users = [
{ name: 'Ada', address: { city: 'London' }, greet() { return `Hi, ${this.name}`; } },
{ name: 'Linus' }
];
for (const user of users) {
console.log(`${user.name}: city=${user.address?.city} greeting=${user.greet?.()}`);
}
console.log(users[7]?.name);
console.log(users[0]?.address?.city.length);?. tests only whether the value immediately to its left is null or undefined, and when it is, the rest of the chain is skipped and the expression becomes undefined.
Worked examples
Short-circuiting skips the whole chain
Shows that a nullish link cancels every later property access and even the argument expressions of a call.
let calls = 0;
function track(label) {
calls++;
return label;
}
const config = null;
console.log(config?.server.ports[0]);
console.log(config?.lookup(track('x')));
console.log('track called:', calls);
const real = { lookup: (v) => `got ${v}` };
console.log(real?.lookup(track('y')));
console.log('track called:', calls);Example explained
Line 1config is null, so config?.server ends the chain immediately and .ports[0] is never evaluated.
Line 2config?.lookup(track('x')) never runs track, which is why calls is still 0 — short-circuiting skips the argument list too.
Line 3real is an object, so the same shaped expression runs for real and track('y') raises calls to 1.
Only null and undefined trigger it
Demonstrates that falsy-but-present values are accessed normally, and that guarding the wrong link still throws.
const data = { count: 0, label: '', items: [], user: null };
console.log(data.count?.toFixed(2));
console.log(data.label?.length);
console.log(data.items?.[0]);
console.log(data.user?.name);
try {
console.log(data?.user.name);
} catch (err) {
console.log(err.constructor.name);
}Example explained
Line 1data.count is 0, which is not nullish, so toFixed(2) actually runs and returns '0.00'.
Line 2data.items?.[0] is the bracket form; the array exists but is empty, so the index lookup itself gives undefined.
Line 3data.user?.name short-circuits on null and evaluates to undefined.
Line 4data?.user.name guards the wrong link: data exists, so evaluation continues into null.name and throws TypeError.
Optional calls for hooks
Uses ?.() to invoke callbacks only when they are supplied, and shows what it does not protect against.
function render(node, hooks = {}) {
hooks.onStart?.(node);
const html = `<${node}>`;
hooks.onEnd?.(html);
return html;
}
console.log(render('div'));
console.log(render('p', { onEnd: (h) => console.log('built', h) }));
const bad = { onStart: 'not a function' };
try {
bad.onStart?.('x');
} catch (err) {
console.log(err.constructor.name);
}Example explained
Line 1hooks defaults to {}, so hooks.onStart is undefined and ?.() skips the call rather than throwing.
Line 2The second call supplies onEnd, so 'built <p>' prints before render's return value reaches the outer console.log.
Line 3bad.onStart holds a string, which is not nullish, so ?.() proceeds and throws — the guard checks for null/undefined, not for callability.
Important notes
?.() guards a nullish callee, not a non-callable one; if the property holds a string or number the call still throws TypeError.
It does not help with undeclared variables: maybe?.x throws ReferenceError if maybe was never declared, so use typeof maybe for that check.
Common mistakes
Guarding only the first link, as in data?.user.name: data was never the missing part, so this still throws TypeError while looking safe.
Sprinkling ?. everywhere to stop crashes: a misspelled key like res?.daat?.items now returns undefined silently and surfaces much later as 'undefined' on the page or NaN in a total.
Using it as an assignment target: user?.profile.name = 'Ada' is a SyntaxError, so the whole script fails to parse instead of failing at that line.
Try it yourself
Change, predict, then run
In a browser console, build an array of three order objects where the first has customer.email, the second has a customer with no email, and the third has no customer at all, then log order.customer?.email?.toUpperCase() for each. Remove the second ?. and note which order now throws and why.
Open the JavaScript workspaceCheck your understanding
For const a = { b: null };, what is the result of evaluating a?.b?.c.d?
- undefined, because the ?. after b abandons the rest of the chain
- null, because that is the last real value the chain reached
- A TypeError, because .c and .d are not guarded with ?.
- A TypeError, because a?.b already produced undefined
Show answer
a is not nullish, so evaluation continues; a.b is null, so the ?. after b short-circuits the whole remainder and .c and .d are never evaluated, giving undefined rather than null. The TypeError answer about unguarded .c and .d assumes every link needs its own ?., but a guard covers everything to its right — only an unguarded nullish link throws, and there is none here.