Ch. 1 · JavaScript

Nullish Coalescing (??) vs OR (||) in JavaScript: Defaults Explained

Why || replaces 0, empty strings and false with defaults while ?? only replaces null and undefined, plus ??=, precedence and parameter defaults.

~6 min readbeginnerupdated Oct 6, 2026

“What is the difference between ?? and ||?” is a staple beginner JavaScript interview question, and it often comes wrapped in a bug: “A user set their volume to 0, saved, and the app reset it to 50. Find the problem.” The answer is almost always a || used for a default value. Interviewers like this question because it checks whether you know JavaScript’s falsy values precisely, and whether you think about valid-but-falsy data such as zero, empty strings and false.

A strong answer also covers the related default mechanisms, which do not all follow the same rule: default parameters, destructuring defaults and the logical assignment operators.

Before you start

You should know what truthy and falsy mean: in a boolean context, false, 0, -0, 0n, '', null, undefined and NaN are falsy, and everything else, including '0', [] and {}, is truthy. You should also know that || and && return one of their operands, not necessarily a boolean.

The short answer

a || b returns b whenever a is falsy, so legitimate values like 0, '' and false get replaced. a ?? b returns b only when a is null or undefined, so it is the right operator for “use this default when the value is missing.” Default parameters and destructuring defaults are narrower still: they apply only to undefined, not null. Use || only when every falsy value really should be treated as “no value.”

How it works

Both operators short-circuit: the right side is evaluated only if needed. The difference is the test applied to the left side.

const values = [0, '', false, NaN, null, undefined, 'set'];
for (const v of values) {
  console.log(String(v).padEnd(9), '||', String(v || 'default').padEnd(7), '??', v ?? 'default');
}
// 0         || default ?? 0
//           || default ??
// false     || default ?? false
// NaN       || default ?? NaN
// null      || default ?? default
// undefined || default ?? default
// set       || set     ?? set
JavaScript

The empty-string row shows ?? returning the empty string, which prints as nothing. || replaced five values that ?? kept.

Default parameters and destructuring defaults use a third rule: only undefined triggers the default. Passing null explicitly means “I am providing a value, and it is null.”

function greet(name = 'guest') {
  return `hi ${name}`;
}
console.log(greet()); // hi guest
console.log(greet(undefined)); // hi guest
console.log(greet(null)); // hi null

const { limit = 10 } = { limit: null };
console.log(limit); // null
JavaScript

That null case surprises many developers working with JSON APIs, where missing fields often arrive as null. If you need null to fall back too, apply ?? inside the function.

Step-by-step walkthrough

Step 1: Decide which values count as missing

Before choosing an operator, ask what “no value” means for this field. For a volume level, 0 is a real setting. For a display name, perhaps an empty string should fall back to “Anonymous”. For a boolean preference, false is a deliberate choice. Usually only null and undefined mean missing, which points to ??.

Step 2: Apply ?? to settings

function loadSettings(saved) {
  return {
    volume: saved.volume ?? 50,
    nickname: saved.nickname ?? 'Player',
    notifications: saved.notifications ?? true,
  };
}
console.log(loadSettings({ volume: 0, nickname: '', notifications: false }));
// { volume: 0, nickname: '', notifications: false }
console.log(loadSettings({}));
// { volume: 50, nickname: 'Player', notifications: true }
JavaScript

If you also want to reject an empty nickname, express that rule separately and visibly, for example saved.nickname?.trim() || 'Player', so readers can see it is intentional.

Step 3: Use the logical assignment operators

a ??= b assigns b only when a is nullish; a ||= b assigns when a is falsy; a &&= b assigns when a is truthy. They short-circuit too: when no assignment is needed, the right side never runs and no write happens.

const options = { retries: 0, timeout: null };
options.retries ??= 3;
options.timeout ??= 5000;
options.label ||= 'untitled';
console.log(options); // { retries: 0, timeout: 5000, label: 'untitled' }

let calls = 0;
const expensive = () => { calls++; return 'computed'; };
const cache = { value: 'cached' };
cache.value ??= expensive();
console.log(calls); // 0
JavaScript

Step 4: Mind precedence and mixing

The language forbids combining ?? directly with || or && because the intended grouping is ambiguous. You must add parentheses. Also note that ?? has lower precedence than comparison operators, so arithmetic or comparisons bind first.

let err;
try {
  eval('null || undefined ?? "x"');
} catch (e) {
  err = e.constructor.name;
}
console.log(err); // SyntaxError
console.log((null || undefined) ?? 'x'); // x

const count = null;
console.log((count ?? 0) + 1); // 1
console.log(count ?? 0 + 1); // 1
const n = 5;
console.log(n ?? 0 + 1); // 5
JavaScript

The last two lines show why parentheses matter: count ?? 0 + 1 parses as count ?? (0 + 1). It happens to print 1 for null, but for 5 it prints 5, not 6.

Worked scenario

An e-commerce cart let users pick a quantity and a discount percentage. Support tickets said that free promotional items (price 0) showed as full price and a 0 percent discount became 10 percent.

function lineTotalBroken(item) {
  const price = item.price || item.listPrice;
  const discount = item.discount || 10;
  return price * (1 - discount / 100);
}
console.log(lineTotalBroken({ price: 0, listPrice: 30, discount: 0 })); // 27
JavaScript

Both defaults used ||, so 0 was treated as missing. The fix uses ??, which keeps real zeros and only falls back when the field is absent.

function lineTotal(item) {
  const price = item.price ?? item.listPrice;
  const discount = item.discount ?? 10;
  return price * (1 - discount / 100);
}
console.log(lineTotal({ price: 0, listPrice: 30, discount: 0 })); // 0
console.log(lineTotal({ listPrice: 30 })); // 27
console.log(lineTotal({ price: null, listPrice: 30, discount: null })); // 27
JavaScript

The last call shows why ?? beats a default parameter or destructuring default here: the API sent null for missing fields, which = 10 would not have caught.

Common mistake

  • “?? checks for falsy values.” It only checks for null and undefined.
  • “Default parameters handle null.” They handle only undefined.
  • “?? and || are interchangeable for strings.” Not when an empty string is valid input, such as a cleared search box.
  • Treating NaN as missing with ??. NaN ?? 0 is NaN. If invalid numbers should fall back, check with Number.isNaN explicitly.
  • Mixing ?? with || without parentheses. It is a syntax error, not a precedence puzzle.
  • Assuming ?? is a type check. It does not validate. 'abc' ?? 5 is 'abc', even if you expected a number.

Verify the behavior

These assertions document the three different default rules side by side.

import assert from 'node:assert/strict';

assert.equal(0 || 5, 5);
assert.equal(0 ?? 5, 0);
assert.equal('' ?? 'x', '');
assert.equal(null ?? 'x', 'x');

const withParam = (v = 'd') => v;
assert.equal(withParam(undefined), 'd');
assert.equal(withParam(null), null);

const { a = 'd' } = { a: null };
assert.equal(a, null);

let x = 0;
x ||= 9;
assert.equal(x, 9);
let y = 0;
y ??= 9;
assert.equal(y, 0);
console.log('defaults behave as described');
// defaults behave as described
JavaScript

Run it with node defaults.mjs.

Follow-up questions

How does ?? relate to optional chaining? They pair naturally: user?.profile?.city ?? 'Unknown'. Optional chaining yields undefined when a link is missing, and ?? supplies the fallback.

Is ?? the same as x !== null && x !== undefined ? x : y? Yes, except that ?? evaluates x only once, which matters when x is a function call or getter.

What about document.all? It is a legacy browser object that is falsy and loosely equal to null, but document.all ?? 'x' returns document.all, because ?? checks for actual null or undefined.

When is || still right? When every falsy value genuinely means missing, such as showing a placeholder for any empty or absent label.

Interview exercise

What does each line print, and which ones would you rewrite in production code?

const cfg = { port: 0, host: '', debug: false, retries: null };
console.log(cfg.port || 3000);
console.log(cfg.host ?? 'localhost');
console.log(cfg.debug ?? true);
console.log(cfg.retries ?? 3);
console.log(cfg.missing?.value ?? 'none');
// 3000
//
// false
// 3
// none
JavaScript

Answer and reasoning

cfg.port || 3000 prints 3000 because 0 is falsy. Port 0 can be meaningful (it asks the OS for any free port), so this should become ??. cfg.host ?? 'localhost' prints an empty line because '' is not nullish; whether that is a bug depends on the rule. An empty host is almost certainly a mistake, so a deliberate cfg.host || 'localhost' or validation that rejects empty strings is clearer. cfg.debug ?? true prints false, correctly respecting the explicit choice; || would have forced it to true. cfg.retries ?? 3 prints 3 because null is nullish. The optional chain yields undefined for the missing object, and ?? supplies 'none'. The reasoning interviewers want is not memorising the table but naming, per field, which values mean “missing.”

Continue learning

Try more JavaScript interview questions and quiz yourself with the JavaScript MCQs. Related notes: optional chaining with nullish coalescing and destructuring defaults. Official reference: Nullish coalescing operator on MDN.

More in JavaScript

esc