JavaScript ?? vs ||: How to Handle null, undefined, 0, and Empty Strings Correctly
Volume 0 turning into 50 and an empty nickname becoming 'Anonymous' are usually caused by ||. The difference between ?? and ||, optional chaining with ?., ??=, when default parameters actually apply, and the right way to check for null, with an interactive demo.
I set the volume to 0. Why is it 50?
Here's a common pattern: load the user's settings and fall back to a default when a value is missing.
const settings = { volume: 0, nickname: "", darkMode: false };
const volume = settings.volume || 50;
const nickname = settings.nickname || "Anonymous";
const darkMode = settings.darkMode || true;
The result:
50 // the volume the user deliberately set to 0 became 50
"Anonymous" // the nickname they left empty became "Anonymous"
true // dark mode they turned off is now on
All three of the user's choices were ignored. The cause is ||, and the fix is ??.
const volume = settings.volume ?? 50; // 0
const nickname = settings.nickname ?? "Anonymous"; // ""
const darkMode = settings.darkMode ?? true; // false
The difference between || and ??
|| uses the right side when the left value is falsy. ?? (the nullish coalescing operator) uses the right side only when the left value is null or undefined.
The falsy values in JavaScript are false, 0, "" (empty string), NaN, null, and undefined. || treats all six as "no value." But 0, "", and false are usually perfectly valid values.
| Left value | value || "default" | value ?? "default" |
|---|---|---|
| 0 | "default" | 0 |
| "" | "default" | "" |
| false | "default" | false |
| NaN | "default" | NaN |
| null | "default" | "default" |
| undefined | "default" | "default" |
Click through the values in the preview below to compare them yourself.
There's one question to ask: "Are 0, an empty string, and false valid values here?" If yes, use ??. If not (you want empty values replaced with the default too), use ||. For most settings, numbers, and form input, ?? is the right choice.
?. optional chaining: don't crash when something in the middle is missing
When you work with objects whose intermediate levels might be empty, like API responses, this is the most common error:
const user = { profile: null };
user.profile.name;
// TypeError: Cannot read properties of null (reading 'name')
?. (optional chaining) stops and returns undefined instead of throwing when the value before it is null or undefined.
user.profile?.name; // undefined
user.profile?.name ?? "No name"; // "No name"
?. and ?? work as a pair: read safely with ?., then fill in a default with ??.
| Where | Syntax | Example |
|---|---|---|
| Property | obj?.prop | user?.name |
| Bracket access | obj?.[key] | order.items?.[0] |
| Function call | fn?.() | options.onSuccess?.() |
// Call the callback only if it exists
options.onSuccess?.(result);
// Price of the first item in the cart, or 0
const firstPrice = order.items?.[0]?.price ?? 0;
Don't put ?. after every dot out of habit. With res?.data?.user?.address?.city, a value that must never be empty quietly becomes undefined when it is, and the bug hides. Use it only on the steps that can genuinely be empty, so a missing required value fails fast with an error.
??=, ||=, &&=: fill in only when empty
Logical assignment operators are a short way to write "assign only if a condition holds."
const config = { retries: 0, timeout: undefined };
config.retries ??= 3; // 0 stays (it's not null/undefined)
config.timeout ??= 5000; // it's undefined, so 5000 goes in
// { retries: 0, timeout: 5000 }
| Operator | Assigns when | Long form |
|---|---|---|
| a ??= b | a is null/undefined | a ?? (a = b) |
| a ||= b | a is falsy | a || (a = b) |
| a &&= b | a is truthy | a && (a = b) |
The same trap applies here. Write config.retries ||= 3 and the user's deliberate 0 (no retries) becomes 3.
Default parameters and destructuring defaults apply only to undefined
Default parameter values cover even less than ??. They apply only to undefined; null passes straight through.
function greet(name = "guest") {
return `Hello, ${name}!`;
}
greet(); // "Hello, guest!"
greet(undefined); // "Hello, guest!"
greet(null); // "Hello, null!" ← the default doesn't apply
greet(""); // "Hello, !"
Destructuring defaults work the same way.
const { page = 1, size = 10 } = { page: null, size: undefined };
page; // null (no default)
size; // 10
API responses often contain null. If you want null replaced with a default too, use ?? instead of a default parameter.
function greet(name) {
return `Hello, ${name ?? "guest"}!`;
}
| Approach | undefined | null | 0, "", false |
|---|---|---|---|
| Default parameter / destructuring default | Default | Kept | Kept |
| ?? | Default | Default | Kept |
| || | Default | Default | Default |
Checking whether a value is "missing," correctly
null and undefined at once: == null
Normally you should use ===, but when you want to check for both null and undefined, == null is short and precise.
value == null; // true only when value is null or undefined
0 == null; // false
"" == null; // false
false == null; // false
if (!value) also filters out 0 and "", so when you mean "is this empty?", value == null is closer to what you want.
Confusing type checks
| What you want | ❌ Easy to get wrong | ✅ Correct |
|---|---|---|
| Is it an array? | typeof arr === "object" | Array.isArray(arr) |
| Is it null? | typeof v === "object" (null is also "object") | v === null |
| Is it NaN? | v === NaN (always false), isNaN(v) (the string "abc" is true too) | Number.isNaN(v) |
?? doesn't catch NaN
A common mistake when combining number conversion with ??:
const qty = Number("abc"); // NaN
qty ?? 1; // NaN ← NaN isn't null/undefined, so it stays
Number.isNaN(qty) ? 1 : qty; // 1
Also remember that Number("") and Number(null) become 0, not NaN. That's where the bug of an empty input silently turning into 0 comes from.
You can't mix ?? and || without parentheses
null || undefined ?? "x"; // SyntaxError
(null || undefined) ?? "x"; // "x"
The language blocks this so there's no confusion about which runs first. If you need both, make the order explicit with parentheses.
Summary: why this is worth knowing
| Situation | Correct handling |
|---|---|
| Default value (0, "", false are valid) | value ?? default |
| Replace every empty value with a default | value || default |
| Access an object whose middle may be empty | obj?.a?.b |
| Call a callback only if it exists | fn?.() |
| Fill in only when empty | a ??= b |
| A parameter that should default on null too | ?? instead of a default parameter |
| Check for null/undefined | value == null |
| Check for arrays / NaN | Array.isArray / Number.isNaN |
0, an empty string, and false are often values the user chose, not "nothing." The habit of filling in defaults with || silently erases them, and because nothing throws, these bugs are found late. When you add a default, reach for ?? first, and use || only when you clearly mean "replace empty values with the default too."