?. stops evaluating as soon as it meets null or undefined, so a deep read does not throw. ?? supplies a fallback for exactly those two values, unlike ||, which also falls back on 0, '' and false. Choosing between ?? and || is the difference between preserving a valid falsy value and silently discarding it.
Before you start
You should be comfortable with truthiness, &&/||, and property access on nested objects. Every example runs in a modern Node.js version or browser.
Step-by-step walkthrough
Step 1: Guard each optional step
Write order?.customer?.address?.city so a missing customer yields undefined instead of throwing. Each ?. guards only the access immediately after it, so every step that may be absent needs its own operator.
Step 2: Call and index safely
handler?.() calls the function only when it is not nullish, and rows?.[0] guards an index access. Both avoid a TypeError on a missing middleware or empty list, and both short-circuit the whole chain.
Step 3: Pick the right fallback
Use ?? when a falsy value is meaningful: const retries = config.retries ?? 3 keeps 0. Use || only when you intend every falsy value to trigger the fallback. Because mixing ?? with || without parentheses is a syntax error, group the intent explicitly if you must combine them.
Worked scenario
Run this with Node.js. The nullish operator keeps 0, while the logical OR replaces it.
const config = { retries: 0, name: '' };
console.log(config.retries ?? 3); // 0
console.log(config.retries || 3); // 3
console.log(config.user?.address?.city); // undefinedWalk through the example
retries is 0, which is nullish-neither null nor undefined, so ?? keeps it while || treats 0 as falsy and returns 3. The chained access finds no user, so it short-circuits to undefined rather than throwing. Notice that ?? distinguishes “value is zero” from “value is missing”, which is exactly what a default should do.
Common mistake
Using || to set defaults for numeric settings, turning a deliberate 0 or a valid '' into the fallback. A second mistake is assuming ?. continues a chain past a defined null: actually null?.x returns undefined, but null?.x.y still short-circuits, so no error occurs. The guard is on propagation, not a value check.
Verify the behavior
Assert that 0, '', false and NaN are preserved by ?? and replaced by ||. Test obj?.method?.() with a missing method and confirm it returns undefined without calling anything. Confirm arr?.[0] returns undefined for an undefined array but still reads a real first element.
Interview exercise
A settings screen shows “off” for a user who set retries to 0. The code reads settings.retries || 3. What happened?
Answer and reasoning
The || operator treated 0 as falsy and replaced it with the default 3, so the user’s explicit value was discarded. Switch to settings.retries ?? 3, which overrides only when the value is null or undefined. This is the standard signal that ?? is the correct default operator whenever falsy-but-valid values exist.
Continue learning
See related defaulting behavior in nullish defaults and read the MDN optional chaining reference. Continue with the JavaScript MCQs.