JS Sketchbooksee JavaScript think ✏️
← back to phase 8

8.4 — ES6+ grab bag: ?. and ??

Back in 2.4 you learned || defaults and && guards — and a note said modern code upgrades them to ?? and ?., “Phase 8.” Welcome to the payoff. These two tiny operators kill the single most common error in JavaScript: reading a property off undefined — and fix a genuine bug hiding in ||.

Every API response, every optional field, every “this user has no pet” — this is the grammar real data is read with.

watch it happen
const user = { name: "Ada", pet: { name: "Rex" } };

// user.cat.name   ← TypeError! program dead
console.log(user.pet?.name);
console.log(user.cat?.name);

const volume = 0;
console.log(volume || 50);
console.log(volume ?? 50);

const label = user.cat?.name ?? "no pet";
console.log(label);

Meet the most common error in the language. user has no cat property, so user.cat evaluates to undefined (1.7) — and asking undefined for .name doesn’t answer undefined. It CRASHES: TypeError, program dead, nothing below ever runs.

the lookup chain, hop by hopuser.cat → undefined.name 💥undefined has no properties — asking it for .name kills the whole program
under the hood

The deeper story, with the real names for things — this part is what turns “I saw it” into “I can explain it.”

The family has two more members: user.greet?.() calls a method only if it exists, and arr?.[0] guards bracket lookups. Same rule everywhere: null/undefined on the left → stop, answer undefined.

The stop is total: in a?.b.c.d(), if a is missing, then .b, .c and the call all never happen — including any side effects they would have had. That’s why it’s called short-circuiting, the same word from 2.4.

A designed-in speed bump: you may not mix || and ?? bare — a || b ?? c is a SyntaxError. The language forces parentheses so nobody has to memorize which binds tighter. When in doubt, parenthesize — 1.10’s advice, now enforced by the grammar.

Job note: document.querySelector answers null when nothing matches (7.2), so el?.textContent is daily DOM grammar. And in API testing, response fields are optional by nature — body.user?.address?.city ?? "unknown" is the shape of half the assertions you’ll write on the job.

your turn

⌨️ the crash-proof pet reader

A user list where some users have a pet and some don’t — exactly the shape every real API sends. Read every pet name without a single crash and without a single if.

requirements:

  • Create users: an array of three objects — { name: "Ada", pet: { name: "Rex" } }, { name: "Mo" }, and { name: "Liv", pet: { name: "Tuna" } }.
  • Write petLabel(user) returning the user’s pet’s name — or "no pet" when there isn’t one. No if allowed: today’s two operators do it in one line.
  • Loop over users (for...of) and print each label, in order.

when you press RUN, the console must show exactly:

Rex
no pet
Tuna

✏️ Quick check 1

const volume = 0. What does console.log(volume ?? 50) print?

✏️ Quick check 2

user has no cat property. What does console.log(user.cat?.name) print?

✏️ Quick check 3

Which operator treats the empty string "" as missing and replaces it — || or ?? ?

teach it back

🗣️ Now teach it back

Explain to a friend why user.cat.name crashes but user.cat?.name doesn’t, and why volume ?? 50 is safer than volume || 50 when volume is 0.

Write it as if your friend is sitting next to you. Saved to your journal — future-you will use these notes to teach others.

a few sentences, minimum — you’ve got this
to remember
?. = the dot with a guard: left side null/undefined → STOP, answer undefined; otherwise a normal dot. The stop abandons the entire rest of the chain (?.() and ?.[ ] exist too).
?? = the honest default: only null/undefined count as missing — 0 and "" survive. (|| replaces anything falsy; mixing || and ?? without parens is a SyntaxError by design.)
The pair user.pet?.name ?? "no pet" reads optional data in one line. Use ?. where absence is normal — not as armor on every dot; silent undefineds hide real bugs.
next: 8.5 A taste of TypeScript →