The nullish coalescing operator (??, sometimes described as “double question mark”) picks a fallback only when the left-hand side is null or undefined. That makes it ideal for preserving other falsy-but-meaningful values such as 0, "", or false, which logical OR (||) would accidentally replace. It landed in ES2020, chains like other binary operators—use parentheses when mixing it with || / &&—and complements patterns from JavaScript optional parameters and default values when you want function arguments to behave the same way.
Tested on: Node.js v20.18.2. A short note after each snippet describes what you should see in the console.
Quick reference
?? only treats null and undefined as missing; || treats any falsy value as missing—pick based on whether 0, "", or false are valid data.
| Operator | Replaces when left is |
|---|---|
a ?? b |
null or undefined |
a || b |
Any falsy value (0, "", false, null, undefined, NaN, …) |
What does the double question mark mean in JavaScript?
In expressions, a ?? b means “use a when it is defined, otherwise use b.” Formally it is the nullish coalescing operator. Only null and undefined on the left count as missing; numbers, strings, booleans, objects, and symbols—including values like 0, "", or false—are all kept on the left because they are still defined values.
Default values: manual check versus ??
This first snippet compares an old-style null check with ??. When the left side is null, the ternary-style expression and ?? both fall back to the default string.
const someVariable = null;
const someVariable2 = 'User One';
const result =
someVariable !== null && someVariable !== undefined
? someVariable
: 'default value';
const result2 = someVariable2 ?? 'default value';
console.log(result, result2);You should see one line: default value User One.
Double question mark in javascript for users, URLs, and counts
These patterns match how people use double question mark in js day to day: pick a display name, substitute a URL when a string is missing, and understand how ?? interacts with numbers taken from the DOM or an API.
const user = 'John Doe';
const username = user ?? 'Guest';
const emptyCart = [];
console.log('length only', emptyCart.length ?? 10);
const missingCount = null;
console.log('nullish default', missingCount ?? emptyCart.length);
const imageUrl = null;
const imgSrc = imageUrl ?? 'https://example.com/default.jpg';
console.log(username, imgSrc);You should see three lines: length only 0, then nullish default 0, then John Doe https://example.com/default.jpg.
[].length is the number 0, and 0 is not nullish, so emptyCart.length ?? 10 stays 0. That is different from “use 10 when the array is empty,” which needs an explicit length check, not ?? alone. On the other hand, missingCount ?? emptyCart.length shows ?? when the left side really is null: it substitutes the right-hand expression.
Why nullish coalescing is not the same as ||
Logical OR returns the first truthy operand. Nullish coalescing returns the first operand unless it is null or undefined. The difference matters whenever 0, "", or false are legitimate data.
console.log(0 || 100);
console.log(0 ?? 100);
console.log('' || 'default');
console.log('' ?? 'default');
console.log(false || true);
console.log(false ?? true);You should see six lines: 100, 0, default, (blank line for empty string), true, false. The fourth console.log prints an empty line because the resolved value is "".
So || replaces 0 and "" with the fallback, while ?? keeps them. TypeScript uses the same rules for ??, which is why typescript searches for “double question mark” land on the same operator.
Chained defaults without throwing away zero
Chaining ?? works like a waterfall: each step only runs when the previous value was null or undefined.
const preferences = { maxWidth: undefined };
const maxWidth = 0;
const max = maxWidth ?? preferences.maxWidth ?? 500;
console.log(max);
const maxWidth2 = undefined;
const max2 = maxWidth2 ?? preferences.maxWidth ?? 500;
console.log(max2);You should see two lines: 0, then 500.
Here maxWidth is 0, which is defined, so the chain stops immediately. When maxWidth2 is undefined, evaluation continues until it reaches 500.
Object fields: only nullish values fall through
This object shows how ?? reads each property. Empty string, numeric zero, and boolean false stay put because they are not nullish. A missing property and an explicit null pick up the right-hand default.
const options = { timeout: 0, title: '', verbose: false, n: null };
console.log(options.timeout ?? 1000);
console.log(options.title ?? 'Untitled');
console.log(options.verbose ?? true);
console.log(options.quiet ?? false);
console.log(options.n ?? 10);You should see five lines: 0, then a blank line (empty title), then false, false, 10.
Mixing ?? with || or &&
The grammar forbids mixing ?? with || or && without parentheses, because different groupings change the result. Parentheses make the intent explicit:
const a = null;
const b = 2;
const c = 3;
console.log((a ?? b) || c);
console.log(a ?? (b || c));
try {
new Function('let a=null,b=2,c=3; return a ?? b || c');
} catch (e) {
console.log(e.name);
}You should see three lines: 2, 2, SyntaxError.
Summary
?? supplies defaults only for null and undefined; || conflates those with every other falsy value—so 0 ?? 1 stays 0, while 0 || 1 becomes 1.
Because the grammar forbids mixing ?? with || or && without parentheses, another common question is why a ?? b || c is a syntax error: the language refuses ambiguous parse trees, so add explicit grouping to show whether you meant (a ?? b) || c or a ?? (b || c). For older browsers, transpilers such as Babel rewrite ?? like other modern syntax.
