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 valuevalue || "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 ??.

WhereSyntaxExample
Propertyobj?.propuser?.name
Bracket accessobj?.[key]order.items?.[0]
Function callfn?.()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 }
OperatorAssigns whenLong form
a ??= ba is null/undefineda ?? (a = b)
a ||= ba is falsya || (a = b)
a &&= ba is truthya && (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"}!`;
}
Approachundefinednull0, "", false
Default parameter / destructuring defaultDefaultKeptKept
??DefaultDefaultKept
||DefaultDefaultDefault

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

SituationCorrect handling
Default value (0, "", false are valid)value ?? default
Replace every empty value with a defaultvalue || default
Access an object whose middle may be emptyobj?.a?.b
Call a callback only if it existsfn?.()
Fill in only when emptya ??= b
A parameter that should default on null too?? instead of a default parameter
Check for null/undefinedvalue == null
Check for arrays / NaNArray.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."