Optional Chaining
Reading a property of null or undefined throws a TypeError. For years this meant writing chains of a && a.b && a.b.c to traverse data safely. Optional chaining (?.), added in ES2020, replaces that ritual with a single operator that returns undefined the moment any link in the chain is null or undefined.
The basic operator
const user = { profile: { name: "Ada" } };
const empty = {};
console.log(user.profile?.name); // 'Ada'
console.log(empty.profile?.name); // undefined — no error
console.log(empty.missing?.deep?.path); // undefinedThe expression a?.b reads as: "if a is null or undefined, the whole expression is undefined; otherwise, evaluate a.b."
Three forms — property, call, index
?. has three shapes that match the three kinds of access JavaScript supports.
obj?.prop— read a property only ifobjis not nullish.obj?.()— call a function only if it exists. Works for "maybe-defined" callbacks.obj?.[key]— bracket access only ifobjis not nullish. Use for dynamic keys.
function notify(callback, channel) {
// Call callback only if it was provided
callback?.("hello");
// Bracket access — useful when key is computed
const url = channel?.["webhook-url"];
return url;
}
console.log(notify(msg => console.log("got:", msg)));got: hello undefined
Short-circuiting
Once a ?. decides the chain is undefined, everything to the right is skipped — including function calls. That is what makes long chains safe.
const data = null; // None of the .toUpperCase() or [0] is evaluated const result = data?.user?.name?.toUpperCase()?.[0]; console.log(result); // undefined
?. vs &&
The classic safe-access pattern a && a.b && a.b.c short-circuits on any falsy value: null, undefined, 0, "", NaN, false. Optional chaining only short-circuits on null and undefined. That precision matters.
const obj = { count: 0 };
// Old style — bails out because count is 0 (falsy)
console.log(obj.count && obj.count.toString()); // 0 (and never calls toString)
// New style — count is defined, so call proceeds
console.log(obj.count?.toString()); // '0'What ?. returns when the chain breaks
The result of a broken chain is always undefined — never null, even if the missing link was null. That uniformity makes downstream checks easier.
const data = { user: null };
console.log(data.user?.name); // undefined — not null
// Combine with default via nullish coalescing
const name = data.user?.name ?? "guest";
console.log(name); // 'guest'Where ?. does not help
It does not assign.
a?.b = 1is a syntax error — there is no "optional set".It does not delete with
delete a?.bin all engines historically — modern engines allow it, but the meaning is "skip if a is nullish".It is not a check for missing keys.
({ a: undefined }).a?.bshort-circuits even though the key exists.Long chains can hide bugs — if you
?.everywhere, real type errors get silently turned intoundefined.
A realistic example
async function loadComments(id) {
const res = await fetch(`/api/posts/${id}`);
const json = await res.json();
// Server may return { post: null } for an unknown id
const comments = json?.post?.comments ?? [];
return comments.map(c => ({
text: c.body,
author: c.author?.name ?? "anonymous",
avatar: c.author?.avatar?.url,
}));
}The function never throws on missing data — it produces sensible defaults at each level.
When to reach for it
External or user-supplied data where shape is uncertain (JSON, query params, DOM).
Optional callbacks passed into a function.
Accessing properties through the DOM:
document.querySelector("h1")?.textContent.Combining with
??for chain-with-default in one line:user?.settings?.theme ?? "light".