JAVASCRIPT / OPERATORS
Nullish coalescing for fallback values
Use ?? and ??= to write defaults that fire only for null or undefined, so deliberate 0, "" and false values survive.
What you will learn
- Use ?? so a real 0, "" or false survives instead of being swapped for a default
- Choose between ?? and || by asking whether falsy inputs are legal values
- Combine obj?.path ?? fallback to cover a missing path and a null result at once
- Use ??= to fill an object slot only when it currently holds null or undefined
Understanding Nullish coalescing for fallback values
The expression a ?? b evaluates a and hands back b only when a is null or undefined. Those two values are the entire nullish set, and the question ?? asks is "was a value supplied at all", not "is this value truthy". That is the whole point of the operator: absence is a different condition from zero, emptiness, or false.
The || operator triggers on every falsy value, so count || 10 replaces a deliberate 0, name || "anon" replaces a cleared text field, and visible || true can never yield false at all, which is how a visible: false setting quietly gets ignored. With ??, the fallback fires only when the slot is genuinely empty, so a volume of 0 stays 0. Keep || when you really do mean "any falsy input means give me the default", such as rejecting an empty search box, and reach for ?? when only a missing value should be replaced.
?? short-circuits, so the right side runs only when the left is nullish, which makes cache ?? expensiveLoad() safe to write inline. Because ?? and || disagree about which values count as missing, chaining them would be ambiguous to a reader, so the grammar rejects a ?? b || c outright as a SyntaxError and forces you to parenthesize the part you mean. The compound form x ??= y applies the same nullish test before assigning and skips the write entirely, including any setter, when x already holds a non-nullish value.
The nullish test is about the value stored, not about whether a property exists, so an object with an explicitly assigned undefined behaves exactly like an object missing that key.
const config = { retries: 0, prefix: "", timeout: null };
console.log("retries ??:", config.retries ?? 3);
console.log("retries ||:", config.retries || 3);
console.log("prefix ??:", JSON.stringify(config.prefix ?? "log"));
console.log("timeout ??:", config.timeout ?? 1000);
console.log("missing ??:", config.missing ?? "default");?? asks only whether a value is null or undefined, so "missing" stops meaning "falsy".
Worked examples
The fallback is not always evaluated
Shows that the right-hand side only runs when the left-hand side is nullish.
function loadDefault() {
console.log("loadDefault ran");
return "computed";
}
const given = "provided";
const blank = null;
console.log(given ?? loadDefault());
console.log(blank ?? loadDefault());Example explained
Line 1given is a non-nullish string, so loadDefault() is never called and nothing extra is logged.
Line 2blank is null, so loadDefault() runs first and prints its own line before the value comes back.
Line 3The inner log appears above "computed" because the call must finish before console.log receives its argument.
Line 4This is why an expensive default is cheap to write inline: you pay for it only on the missing path.
Filling gaps with ??=
Uses nullish assignment to complete an options object without clobbering falsy but valid entries.
const options = { mode: "fast", depth: 0, name: null };
options.mode ??= "slow";
options.depth ??= 5;
options.name ??= "anonymous";
options.limit ??= 10;
console.log(JSON.stringify(options));Example explained
Line 1mode already holds "fast", which is non-nullish, so no assignment happens at all.
Line 2depth keeps its 0; the same line written with ||= would have overwritten it with 5.
Line 3name is replaced because null is nullish, even though the key was already present.
Line 4limit is created, since reading a missing property yields undefined and that satisfies the nullish test.
Parentheses and missing paths
Demonstrates the grammar restriction against mixing ?? with || and the common obj?.prop ?? fallback pattern.
const user = { profile: { nickname: "" } };
// user.profile.nickname ?? "guest" || "fallback" <- SyntaxError if uncommented
console.log((user.profile.nickname ?? "guest") === "");
console.log(user.settings?.theme ?? "dark");
console.log(user.profile?.nickname?.length ?? -1);Example explained
Line 1The commented line would not parse: ?? beside || needs explicit parentheses, and the failure is at load time.
Line 2nickname is "", which is not nullish, so the fallback is skipped and the comparison holds.
Line 3user.settings?.theme yields undefined because the chain stopped early, and ?? turns that into "dark".
Line 4"".length is 0, a real number, so -1 is never reached even though 0 is falsy.
Important notes
?? cannot tell null from undefined; when "explicitly cleared" must differ from "never set", test with === undefined or 'key' in obj.
?? is ES2020 and ??= is ES2021, so old parsers and low build targets report a syntax error rather than degrading quietly.
Common mistakes
Using || for numeric defaults: opts.retries || 3 turns a deliberate 0 into 3, so "do not retry" silently becomes three retries.
Expecting ?? to catch NaN: Number(field) ?? 0 keeps NaN, because NaN is falsy but not nullish, and every later calculation becomes NaN.
Writing a ?? b || c without parentheses: this is a parse error, so the whole script or module fails to load instead of misbehaving on that line.
Try it yourself
Change, predict, then run
In a browser console, write render(o) that computes const w = o.width ?? 100 and const label = o.label ?? "none", then logs JSON.stringify([w, label]). Call it with { width: 0, label: "" } and with {}, then swap both ?? for || and note which of the four printed values change.
Open the JavaScript workspaceCheck your understanding
A function receives { retries: 0 } and must keep that 0 while still defaulting a missing retries to 3. Which expression and reasoning are both correct?
- opts.retries || 3, because || replaces only missing values
- opts.retries ?? 3, because ?? checks whether the key is present on the object
- opts.retries ?? 3, because ?? falls back only when the value is null or undefined
- opts.retries || 3, because 0 is not falsy in a boolean position
Show answer
?? compares the value against null and undefined only, so the deliberate 0 passes through while a missing key reads as undefined and becomes 3. Option 2 is tempting because it names the right operator, but the reasoning is wrong: ?? never inspects key presence, so { retries: undefined } still falls back to 3 even though the key exists. Both || options fail because 0 is falsy and would be replaced.