Structured data: arrays and objects · Lesson 28 of 77 · practice
Reaching in safely: optional chaining and defaults
About 65 minutes.
By the end of this lesson you can
- Use ?. to read through a value that might be null or undefined without throwing.
- Say what an optional chain evaluates to when it short-circuits, and how much of the chain it skips.
- Use ?? to supply a fallback for null and undefined only.
- Say why ?? and || give different answers for 0, the empty string and false.
- Choose between a default parameter, a destructuring default and ??, and say when each fires.
- Name the case ?. does not protect you from.
The staircase of guards
Why it matters
Lesson 4 left you with a rule: check anything you intend to step through. Applied honestly to a four-level path, that rule produces code like `if (order && order.customer && order.customer.address) { ... }` — a staircase of checks that is longer than the thing it protects and easy to get subtly wrong.
Two small operators replace the whole staircase. `?.` handles getting there; `??` handles what to say when you did not. Neither adds new behaviour you could not write by hand, which is exactly why Lesson 4 made you write it by hand first.
The interesting part of this lesson is not the syntax, which takes five minutes. It is knowing when reaching in safely is the right thing to do and when it is quietly hiding a real problem.
Optional chaining: stop instead of throw
Explanation
`a?.b` reads `b` only if `a` is neither `null` nor `undefined`. MDN's wording is worth memorising: if the reference is nullish, the expression short circuits and evaluates to `undefined` instead of throwing an error.
The short-circuit covers the whole rest of the chain, not just the next step. In `data.a?.b.c`, if `data.a` is `null` then `.b` and `.c` are never evaluated at all, and the result is `undefined` — no TypeError, even though `.b` has no `?.` of its own. You put a `?.` at each step that can independently be missing.
There is a bracket form for arrays and computed keys, and it keeps the `?.`: `order?.items?.[0]` and `menu?.[key]`. The dot before the bracket looks odd and is required.
What ?. does not do
A common mistake
It does not help with a variable that was never declared. MDN says so explicitly: optional chaining cannot be used on a non-declared root object. `undeclaredVar?.prop` still throws a ReferenceError, because the problem is the name, not the value.
It does not make missing data present. `order?.customer?.name` is `undefined` when the customer is absent, and `undefined` will keep travelling through your program exactly as it did in Lesson 1 — which is why it is usually paired with a fallback.
And it is not free of judgement. Sprinkling `?.` at every step turns a genuinely broken data structure into a quiet `undefined`, so a bug that should have stopped the program at the line that caused it now shows up as an empty box on a screen three days later. Use it where absence is expected and normal.
?? supplies a fallback for missing, not for falsy
Explanation
`value ?? fallback` returns `fallback` when `value` is `null` or `undefined`, and returns `value` in every other case. That is the entire definition.
Compare it with `||`, which returns the right-hand side for any falsy value — `0`, the empty string, `false`, `NaN` as well as `null` and `undefined`. For a volume, a count, a label or a flag, those are all values somebody may legitimately have chosen.
So `count || 42` turns a real zero into 42, and `count ?? 42` keeps the zero. The bug `||` produces is nasty precisely because it is invisible while you test: the default appears to work until the day a user genuinely wants the falsy value. `??` also cannot be mixed with `||` or `&&` in one expression without parentheses — that is a syntax error, and it exists to stop you writing something whose meaning is ambiguous.
Three ways to supply a default, and when each fires
A mental model
A default parameter — `function pageSize(n = 20)` — fires when the argument is missing or is `undefined`. It never fires for `null`.
A destructuring default — `const { city = 'Unknown' } = user` — fires when the property is absent or is `undefined`. It never fires for `null`. Same rule, different place.
`??` fires for `null` and for `undefined`. That is the only one of the three that treats a deliberate `null` as missing, which is why you sometimes see a parameter default and a `??` in the same function: one handles the caller who did not ask, the other handles the caller who asked with an explicit `null`. If your callers never pass `null`, the `??` is dead code and you should drop it — the trade-off is knowing your callers.
The two together
Explanation
The idiom you will write most often is one chain and one fallback: `const city = order?.customer?.address?.city ?? 'Unknown';`.
Read it as two separate jobs. The `?.`s get you as far down the path as the data allows, producing `undefined` if the walk stops early. The `??` then converts that `undefined` into something the rest of your code can use.
Keeping the two jobs separate in your head matters, because they fail differently. If you get `'Unknown'` when you expected a city, the question is where the walk stopped — and printing `order?.customer` and `order?.customer?.address` will tell you in seconds.
When not to be safe
Why it matters
A guard is a claim that absence is normal here. When absence actually means something has gone wrong — a required field missing from a record you just loaded, a function called with no arguments by mistake — swallowing it with `?? 'Unknown'` hides the failure and makes it somebody else's problem later.
The honest alternative is to fail loudly at the boundary: check the data once, where it arrives, and complain immediately if it is not what you were promised. Module 7 is about exactly that, and it is the other half of this lesson.
A workable rule for now: use `?.` and `??` for data that is genuinely optional, and check explicitly for data that is required. If you cannot say which a field is, that is a question about your data, not about your syntax.
Recap
Recap
`?.` short-circuits the rest of the chain to `undefined` when the value it is applied to is `null` or `undefined`, and needs the `?.[` form for indexes. It does not rescue an undeclared variable. `??` substitutes a fallback for `null` and `undefined` only, unlike `||`, which also swallows `0`, `''` and `false`. Default parameters and destructuring defaults fire for `undefined` but never for `null`.
Worked examples
Every output below was produced by actually running the code in the same sandbox your exercises run in — it is not a prediction.
A chain stopping at different points
The same expression against two objects, so you can see where the walk stops and what it produces when it does.
1const order = { customer: { name: "Ada" } };2const empty = {};3 4console.log(order.customer?.name);5console.log(empty.customer?.name);6console.log(empty.customer?.address?.city);7console.log(order.customer?.address?.city);- line 4
The customer exists, so ?. behaves exactly like a plain dot.
- line 5
empty.customer is undefined, so the chain stops here and the whole thing is undefined.
- line 6
Stops at the same place. The later steps are never evaluated, which is why this does not throw.
- line 7
Gets one step further, then stops at the missing address.
What it prints
Adaundefinedundefinedundefined
?? against ||, on values that are falsy but real
Five expressions over one settings object. Three of the values are falsy and were put there on purpose, which is where the two operators part company.
1const settings = { volume: 0, label: "", theme: null };2 3console.log(settings.volume ?? 5);4console.log(settings.volume || 5);5console.log(settings.label ?? "none");6console.log(settings.label || "none");7console.log(settings.theme ?? "light");- line 3
0 is neither null nor undefined, so ?? keeps it. Prints 0.
- line 4
0 is falsy, so || throws it away. A muted volume becomes 5 — a real bug.
- line 5
Prints an empty line: the empty string was kept.
- line 6
The empty string is falsy, so || replaces it.
- line 7
null IS nullish, so ?? substitutes here. Both operators agree on this one.
What it prints
05nonelight
Three defaulting mechanisms meeting null
A default parameter, a destructuring default and ?? given the same awkward value, so the rule about null stops being something to memorise.
1function greet(name = "friend") {2 return "Hello, " + name;3}4 5console.log(greet());6console.log(greet(undefined));7console.log(greet(null));8 9const { size = "M" } = { size: null };10console.log(size);11console.log(null ?? "M");- line 5
No argument at all: the parameter default fires.
- line 6
An explicit undefined counts as missing, so the default fires again.
- line 7
null is a real value, so the default does NOT fire and it is concatenated into the string.
- line 9
Destructuring defaults follow the same rule: null survives.
- line 11
?? is the one that does treat null as missing.
What it prints
Hello, friendHello, friendHello, nullnullM
Check yourself
Not scored, not stored. Getting one wrong is the useful part.
What does this print?
const count = 0;console.log(count ?? 10);console.log(count || 10);
Given `const user = null;` — what does `user?.profile?.name` evaluate to?
Which problem does ?. NOT solve?
What does this print?
const data = { a: null };console.log(data.a?.b.c);
Write it yourself
Exercise · core · 10 tests
Read an order that might not be there
Rewrite these two functions so they never throw, whatever they are handed. `cityOf(order)` returns the customer's city, or 'Unknown' when any step of that path is missing. `firstItemName(order)` returns the name of the first item line, or 'None' when there is not one. Both are called with objects missing pieces, with empty objects, and with null.
Neither function may throw for null, undefined, an empty object, a missing customer, a missing address or an empty items array. A city that is present but null also falls back to 'Unknown'.
Define cityOf and firstItemName at the top level of your code — the tests call them by name.
Exercise · core · 11 tests
Defaults that keep a real zero
The three functions below all use `||` and all three are wrong. `volumeOf(settings)` returns the stored volume or 5, and a stored volume of 0 must survive. `labelOf(settings)` returns the stored label or 'untitled', and a stored empty string must survive. `pageSize(requested)` returns 20 when nothing was requested, and the requested number otherwise — including 0.
volumeOf and labelOf must not throw when handed null or undefined instead of a settings object, and must fall back when the stored value is null. pageSize falls back when called with no argument, with undefined, or with null, and returns the argument for every other value including 0.
Define volumeOf, labelOf and pageSize at the top level of your code — the tests call them by name.
Worth remembering
- What does a?.b evaluate to when a is null?
undefined — even though a was null. The rest of the chain is skipped entirely, so a?.b.c does not throw either.
- ?? versus || — what is the difference?
?? substitutes only for null and undefined. || substitutes for any falsy value, so it also swallows 0, the empty string, false and NaN.
- Which defaulting mechanisms fire for null?
Only ??. Parameter defaults and destructuring defaults fire for a missing value or undefined, and never for null.
- What does ?. NOT protect you from?
An undeclared variable — undeclaredVar?.prop is still a ReferenceError. It also does not make missing data appear; pair it with ?? for that.
Check this lesson against the source
We wrote the explanation; we did not invent the facts. This is the page that backs them.
Check there: The sentence that the expression short circuits and evaluates to undefined instead of throwing an error, and the note that optional chaining cannot be used on a non-declared root object.
MDN — Nullish coalescing operator (??)
Check there: That ?? returns its right-hand operand only when the left-hand one is null or undefined, and the comparison with || which returns it for any falsy value.
Check there: That a default parameter is used when the argument is omitted or is undefined.
Prefer to read the source in a structured breakdown? Turn this doc into a breakdown