Skip to content
Chapter 2Lesson 5

The null-safe operators: ?., ??, ??=

JavaScript's operators for missing values: optional chaining, nullish coalescing, and nullish assignment, without defensive chains or falsy-coercion bugs.

Two bugs share a root cause: a value that might be missing. The first is the defensive chain if (user && user.profile && user.profile.address) { … }, retyped on every page until someone reaches for a shorter fix. The second is const pageSize = input.pageSize || 20, which works until a caller passes pageSize: 0 to mean “show no rows” and your function silently renders twenty. A trio of operators fixes both precisely.

?., optional chaining, reads a property or calls a method that might not be there. ??, nullish coalescing, supplies a default only when the value is actually missing. ??=, nullish coalescing assignment, fills a slot the first time it’s needed. They come with one rule: JavaScript rejects ?? mixed with || or && unless you add parentheses.

Last lesson’s guard clauses handle these cases with an if and a return; these operators handle them inline, no extra indentation.

The first operator collapses a defensive chain into a single line.

const city = user?.profile?.address?.city;

Each ?. checks the value to its left before stepping right. The first nullish link short-circuits the whole expression to undefined, so the chain replaces nested && guards and never throws Cannot read property 'address' of undefined. Optional chaining has three forms, each with its own punctuation.

const city = user?.profile?.address?.city;

Use the dotted form at any access where the receiver might be missing.

A ?. checks exactly one step, so one ?. at the front of a chain leaves every later link unguarded.

const city = user?.profile.address.city;

This looks defensive but isn’t. The ?. only guards the access to profile; if user.profile is nullish, .address throws the same Cannot read property 'address' of undefined you were avoiding. The fix is a ?. at every link that could be missing:

const city = user?.profile?.address?.city;

When you review a chain with one ?. followed by plain dots, check whether the types allow the middle links to be nullish. If they do, the chain is one missing field away from a runtime error.

The overuse trap: don’t silence the type system

Section titled “The overuse trap: don’t silence the type system”

The opposite reflex causes more trouble: adding ?. wherever you’re unsure. Every unnecessary ?. silences the type system on its line. Optional chaining is for the nullables the type acknowledges; TypeScript is meant to fail loudly when a field that was non-nullable becomes nullable later. Reach for ?. on a field the type says is required, and you turn that future warning off.

type Order = {
customer: Customer;
lines: Line[];
};
const orderTotal = order.customer?.lines.length;

The type marks customer as required, so its type is Customer, not Customer | undefined. The ?. reads as cautious but isn’t: the day someone marks customer optional to support draft orders, every call site silently returns undefined instead of failing at the line that now needs a second look. The rule:

?. where the type acknowledges nullable; let TypeScript fail at the call site otherwise.

A ?. on a non-nullable field is a hidden hole in your type coverage, not extra safety. The narrowing chapter later in this unit covers when a guard clause that narrows before the access beats a ?. after it.

The exercise below checks the per-link rule on a realistic chain.

Given the types below, which expression fetches the city without throwing when any nullable link is missing — and without silencing a future type error?

type Address = { city: string };
type Customer = { address?: Address };
type Order = { customer?: Customer; lines: Line[] };
declare const order: Order;
order.customer?.address.city
order.customer?.address?.city
order?.customer?.address?.city
order.customer.address.city

?? for “use a default when the value is missing”

Section titled “?? for “use a default when the value is missing””

The rule fits in one sentence: ?? returns the right operand only when the left is null or undefined, while || returns it for any falsy left.

Nullish means genuinely absent: null or undefined. Falsy is wider, also counting legitimate-but-empty values as missing: the empty string, zero, false, and NaN. They overlap on null and undefined and diverge everywhere else. || defaults on every zero, empty string, or false the caller might pass, not just on absence, and you almost never want both behaviors at once.

All three tabs share one shape: the right operand is the default, the left can legitimately be falsy, and || overwrites the value the caller meant.

const pageSize = input.pageSize || 20;
const pageSize = input.pageSize ?? 20;

A caller passing pageSize: 0 means “show no rows,” a valid request for an empty result. || reads 0 as falsy and overwrites it with 20; ?? keeps the zero and defaults only when the field is absent.

Work the full table by hand to fix the distinction. The program runs six inputs, five falsy and one ordinary string, through both || and ?? for twelve outputs. The three rows where the operators disagree are the three bugs from the tabs above.

Predict every line this program prints. Six inputs crossed against `||` and `??` — twelve lines total. The rows where the two operators diverge are the three bugs `||` ships and `??` fixes. Predict what this program prints, then press Check.

const cases = [0, '', false, null, undefined, 'value'];
for (const v of cases) {
console.log(`${JSON.stringify(v)} || 'D' = ${v || 'D'}`);
console.log(`${JSON.stringify(v)} ?? 'D' = ${v ?? 'D'}`);
}

For “use a default when the value is missing,” reach for ??. Reach for || only when “any truthy value short-circuits” is exactly what you want. That case is rare, so write ?? by reflex and treat every || as a deliberate choice. This is the same semantics as the parameter defaults from “Options objects,” which fire only on undefined; ?? gives you that behavior as an operator anywhere in a function body.

x ??= y assigns y to x only when x is nullish. Its canonical use is the cache-or-compute pattern, in three characters.

const cache: Record<string, number> = {};
const getOrCompute = (key: string) => {
cache[key] ??= computeExpensive(key);
return cache[key];
};

That one line fills a key’s slot on the first call and reads the cached value on every call after, saying “fill this slot if it’s empty” instead of an explicit if (cache[key] === undefined) …. Reach for it in any initialize-once, reuse-after pattern.

??= evaluates its right operand only when the left is nullish, so computeExpensive(key) never runs on a cache hit — which is the point when that operand is a function call, an API request, or a database read. The long-hand cache[key] = cache[key] ?? computeExpensive(key) also short-circuits to the same result, but reads the cache twice and writes every time.

Two siblings round out the set: ||= assigns when the left is falsy, carrying the same 0/''/false traps as ||, and &&= assigns when the left is truthy. Reach for ??= by default, for the same reason you reach for ?? over ||: nullish-not-falsy is almost always what you want. Knowing the other two exist is enough to read them.

This exercise uses all three operators. getConfig reads a deeply optional configuration object and returns a normalized { pageSize, theme }, treating 0 as a real pageSize and '' as a real theme rather than missing values to overwrite. The || reflex fails both; the ?? reflex passes.

Implement getConfig(input). Read input.user.preferences.pageSize and default to 20 when the field is missing — treat 0 as a legitimate user choice, not as 'missing'. Read input.user.preferences.theme and default to 'light' when missing — treat '' as a legitimate cleared value. Return { pageSize, theme }. The expected solution uses ?. for the nested access and ?? for the two defaults.

    Combine ?? with || or && without parentheses and JavaScript refuses to compile it. a || b ?? c is a syntax error.

    const value = a || b ?? c;

    A reader can’t tell whether || binds tighter than ?? or the reverse, so rather than pick an answer half of them would read wrong, the language makes you group explicitly. One set of parens states the grouping you meant:

    const value = (a || b) ?? c;
    const value = a || (b ?? c);

    The two groupings differ. The first runs || first: take the truthy of a or b, then fall back to c only if that is nullish. The second runs ?? first: use a when truthy, otherwise compute b ?? c. Same operators, two meanings.

    JavaScript’s precedence table has too many operators to memorize, and typing parens is cheaper than recalling them. So whenever an expression mixes different kinds of operator, add parens that state the intent, even where the parser doesn’t require them.

    const score = a ?? b + 1;
    const score = (a ?? b) + 1;

    The first line parses as a ?? (b + 1), almost certainly not what was meant; the second says it out loud: take a if it’s set, otherwise b, then add one. A misread precedence costs a runtime bug that casual review won’t catch, while the parens cost nothing. Any time ?? shares an expression with ||, &&, +, or another binary operator, parenthesize.