Data comes back from an API, three properties get read in a row, and the page goes blank. The message is always the same: cannot read properties of undefined. A well-placed question mark fixes it.
Definition
Optional chaining, written as a question mark followed by a dot, reads a property only when the value on its left is neither null nor undefined. Otherwise it stops evaluating right there and returns undefined.
const client = { profile: { city: "Lyon" } };
console.log(client.profile.city); // "Lyon"
console.log(client.account?.iban); // undefined, no error
// Without the operator, the line above would throw:
// TypeError: Cannot read properties of undefinedEverything hangs on the word stop. As soon as an empty value shows up, the rest of the chain is skipped, even if five levels remain. That behavior is called short-circuiting.
Three forms, not one
The operator is not limited to named properties. It has variants for indexes and for function calls, which many people never discover.
| Form | What it protects |
|---|---|
object?.property | Reading a property |
array?.[0] | Access by index or computed key |
fn?.() | The call, when the function is missing |
The third form genuinely helps with optional callbacks: onSuccess?.() calls the function when one was supplied, and does nothing otherwise. No need to wrap the call in a condition anymore.
The operator only works for reading. An assignment such as object?.field = 1 is rejected by the engine with a syntax error. To write, you first have to guarantee that the object exists.
What it does not do
This is the most misunderstood point, and the one producing the quietest bugs. The operator guards against an empty value, not against a typo.
const client = { profile: { city: "Lyon" } };
console.log(client.profle?.city); // undefined, and no warningThe property is misspelled, the chain returns undefined, and the program carries on as if nothing happened. Without the operator, the same typo would have thrown immediately, and therefore visibly. By adding a question mark, you trade a loud crash for a silently wrong result.
The practical rule: use it only where emptiness is a normal, expected case. On data that must always be there, let the error happen.
Frequently asked questions
Is a question mark needed at every level?
No, one is enough at the spot where emptiness is possible. Since evaluation stops at the first empty link, writing a?.b?.c?.d when only b can be missing adds weight without adding safety. Put the operator on the uncertain link, not across the whole chain.
How do I attach a fallback value to it?
With Nullish coalescing (??), its natural companion. Writing client.profile?.city ?? "not provided" covers both needs: the read never crashes, and an absence produces something displayable rather than a visible undefined.
Does it slow execution down?
No, the impact is negligible, and it is sometimes positive since evaluation stops earlier. The real cost lies elsewhere: on deeply nested outside data, it hides structural problems. Our Node.js course shows how to validate an API response on arrival, which beats guarding every single read.