A dot runs through a door without looking. If the door isn't there, it crashes. Question mark dot knocks first. If there is no door, nothing bad happens, and you just get undefined.
The crash
This user has no address. Reading street from a missing address throws an error and stops your code.
const user = {};
user.address.street;
Node and the browser report TypeError: Cannot read properties of undefined (reading 'street'). Here is what happened. user.address is undefined, because the property doesn't exist. Then the code asks undefined for its street. That question makes no sense, so JavaScript throws an error and stops.
This error shows up all the time in real code. Data from a server might leave out a field, a user might skip part of a form, or a list might be empty. A normal dot doesn't look before it reads, so one missing piece stops everything after it.
Knock first with ?.
Put a question mark right after the part that might be missing. Now JavaScript checks first. If the value before ?. is null or undefined, the whole expression stops there and gives undefined. Your code keeps running.
const user = {};
console.log(user.address?.street); // prints: undefined
const mia = { address: { street: "Elm Road" } };
console.log(mia.address?.street); // prints: Elm Road
When the address is there, ?. acts exactly like a normal dot.
The stop is total. Once ?. finds nothing, JavaScript skips the rest of the chain to its right, so it never tries to read more from undefined.
const user = {};
console.log(user.address?.street.toUpperCase()); // prints: undefined
There is no crash on .street or on .toUpperCase(), even though they use normal dots. If address exists, every step runs as usual. You can also chain several, like user?.address?.street, when more than one part might be missing.
Functions and arrays too
For a function that might not exist, write ?. and then the parentheses. For an array that might not exist, write ?. before the square brackets. Nothing there means no crash.
const user = { name: "Mia" };
console.log(user.sayHi?.()); // prints: undefined
console.log(user.pets?.[0]); // prints: undefined
const sam = { pets: ["cat"], sayHi() { return "hi"; } };
console.log(sam.pets?.[0]); // prints: cat
console.log(sam.sayHi?.()); // prints: hi
user.sayHi?.() only calls the function if it is there. This is handy for optional callbacks, like options.onDone?.().
The twist: add a backup with ??
Pair optional chaining with the double question mark to get a backup value instead of undefined.
const user = {};
const street = user.address?.street ?? "unknown";
console.log(street); // prints: unknown
No street, so you get "unknown". The next lesson covers ?? in detail.
When to use it
Use ?. when something is allowed to be missing. Data from a form, a file, or a server often has optional parts, and settings objects often leave things out.
Don't put ?. everywhere, though. If a value must always exist, a loud error helps you find the bug early. Adding ?. there just hides the problem and hands you an undefined later on.
A good habit is to ask "is this allowed to be missing?" for each part of the chain. Put ?. only after the parts where the answer is yes.
Rule of thumb
If something is allowed to be missing, use ?. to read it.
Putting ?. in the wrong spot. user?.address.street still crashes when address is missing, because ?. only protects against the value right before it. Put it after the part that might be missing, as in user.address?.street.
Check yourself
user.address?.street give when user is {}?user.address is undefined, so ?. stops the chain there and the whole expression gives undefined instead of throwing.