Optional Chaining and Safe Access
Mental model: ?. means "if this is null or undefined, stop and give me undefined", nothing more.
Level: beginner · about 9 minutes
const user = { name: 'Ada' };
try {
console.log(user.address.city); // TypeError: Cannot read properties of undefined
} catch (err) {
console.log(err.constructor.name); // 'TypeError'
}
console.log(user.address?.city); // undefined, no throwThe error on the left is the most common runtime error in JavaScript.
Optional chaining (?.) checks the value on its left. If that value is null or undefined, the whole expression short circuits to undefined. Otherwise it carries on as a normal access.
Three forms
const api = { data: { items: [{ id: 1 }] } };
console.log(api.data?.items?.[0]?.id); // 1 property and index
console.log(api.missing?.items?.[0]?.id);// undefined
console.log(api.onError?.('boom')); // undefined, the call is skipped
console.log(api.data?.items?.length); // 1
`obj?.prop`- property access
`obj?.[expr]`- computed or index access
`fn?.(args)`- call only if
fnis not nullish `obj?.method?.()`- the method might be missing as well as the object
Pairing with ??
const settings = { display: { fontSize: 0 } };
console.log(settings.display?.fontSize ?? 16); // 0, a real value wins
console.log(settings.display?.fontSize || 16); // 16, because 0 is falsy
console.log(settings.audio?.volume ?? 50); // 50, genuinely missing
|| falls back on any falsy value
count || 10
// fires for 0, '', false, NaN
?? falls back only on nullish
count ?? 10
// fires for null and undefined onlyUse ?? for defaults on values where 0, "" or false are meaningful. Use || only when every falsy value really should be replaced.
A small deep get
function deepGet(source, path, fallback) {
let current = source;
for (const key of String(path).split('.')) {
if (current === null || current === undefined) return fallback;
current = current[key];
}
return current === undefined ? fallback : current;
}
const data = { user: { roles: ['admin'] } };
console.log(deepGet(data, 'user.roles.0')); // 'admin'
console.log(deepGet(data, 'user.team.name', 'none')); // 'none'
Optional chaining reads a path you know at author time. A helper like this reads a path built at runtime, from configuration or a form field name. You need both, but reach for the syntax first.
function deepSet(target, path, value) {
const keys = String(path).split('.');
const last = keys.pop();
let current = target;
for (const key of keys) {
if (typeof current[key] !== 'object' || current[key] === null) current[key] = {};
current = current[key];
}
current[last] = value;
return target;
}
console.log(deepSet({}, 'a.b.c', 1)); // { a: { b: { c: 1 } } }Writing needs its own helper: ?. cannot appear on the left of an assignment.
When ?. hides the bug instead of fixing it
Hiding the problem
// The response shape changed and
// nobody noticed for three weeks.
const total = res?.data?.cart?.total;
render(total); // undefined, silently
Handling the problem
const total = res?.data?.cart?.total;
if (total === undefined) {
report('cart total missing', res);
return renderEmptyCart();
}
render(total);Optional chaining is right when absence is a normal, expected state. It is wrong when absence means something upstream is broken, because it converts a loud crash into a quiet undefined that spreads.
const o = { a: { b: null } };
console.log(o.a?.b?.c, o.x?.y, o.a.b?.c ?? 'dflt');o.a?.b is null, so the next ?.c short circuits to undefined. o.x?.y short circuits at o.x. The third expression is undefined ?? "dflt", which is "dflt". Nothing throws because every nullish step was guarded.
Try it yourself
Read a shape that keeps changing
const response = {
data: { profile: { name: 'Ada', address: null }, items: [] },
onDone: null,
};
console.log(response.data?.profile?.name); // 'Ada'
console.log(response.data?.profile?.address?.city); // undefined
console.log(response.data?.items?.[0]?.id ?? 'empty');// 'empty'
console.log(response.onDone?.('finished')); // undefined, not called
console.log(response.data?.items?.length ?? 0); // 0
Delete profile from response.data and see which lines still work. Then remove one ?. and watch it throw.
Exercises
Write deepGet
Write deepGet(source, path, fallback) where path is a dotted string such as "user.address.city". Return the value at that path, or fallback when any step is missing or undefined. Array indexes work as numeric segments: "items.0.name". A stored null or 0 is a real value and must be returned as is.
Check yourself
- What does this log?
- a TypeError —
o.ais not nullish, so?.bproceeds and givesundefined. The plain.cthat follows is unguarded, so it throws. Optional chaining only protects the link it is written on. - Which expression returns
0whensizeis0? opts?.size ?? 10—??only falls back fornullandundefined, so a real0is kept. Every||version replaces0with 10, because0is falsy.- When is optional chaining the wrong tool?
- When the value being missing means something upstream is broken and should be reported — It turns a loud failure into a quiet
undefined. That is ideal for genuinely optional data and harmful when the absence is a bug, because theundefinedthen travels somewhere else before anything complains.
Common mistakes
- Writing one
?.and assuming the rest of the chain is protected. - Using
||for defaults, so0,""andfalseget silently replaced. - Sprinkling
?.across a whole file to stop crashes, which hides shape changes instead of fixing them.
Takeaways
?.short circuits toundefinedwhen the value on its left isnullorundefined.- It comes in three forms:
?.prop,?.[expr]and?.(args). - Pair it with
??so meaningful falsy values survive. - Use it where absence is expected, and report the failure where absence is a bug.