In JavaScript, what is the difference between `a ?? b` and `a || b`, and which left-hand values make the two produce different results?
answer
- one checks falsy, the other checks absent
- only two values count as nullish
- a deliberate zero is the classic casualty
- false, 0, empty string, NaN diverge
- NaN survives one of them untouched
basics
~20 sThe ?? operator falls back only when the left side is null or undefined; || falls back on any falsy value. So 0, -0, NaN, the empty string and false are kept by ?? but replaced by ||.
solid answer
~40 sBoth are short-circuiting fallback operators, but they test different things. `a || b` evaluates `a`, converts it to a boolean, and returns `b` whenever `a` is falsy — that set is `false`, `0`, `-0`, `0n`, `""`, `NaN`, `null` and `undefined`. `a ?? b` returns `b` only when `a` is `null` or `undefined`; every other value, including `0` and `""`, is returned as-is. That is why `??` was added in ES2020: the old `value || default` idiom silently destroys legitimate zero, empty-string and `false` settings, so `volume || 50` turns a deliberate mute into 50 while `volume ?? 50` keeps the `0`. Both short-circuit, so the right operand is only evaluated when it is actually needed. Note that `??` cannot be combined with `||` or `&&` without parentheses.
code
javascript · 8 linesconst settings = { volume: 0, label: '', theme: null };
console.log(settings.volume || 50); // 50 -> deliberate mute lost
console.log(settings.volume ?? 50); // 0 -> mute preserved
console.log(settings.label || 'n/a'); // 'n/a'
console.log(settings.label ?? 'n/a'); // ''
console.log(settings.theme ?? 'dark');// 'dark' -> null is nullish
console.log(NaN ?? 1); // NaN -> NaN is not nullishgo deeper
Be able to state plainly that ?? only reacts to null and undefined while || reacts to every falsy value, and to name 0, empty string and false as the values that behave differently.
Explain the exact falsy set versus the two-value nullish set, show the silent default bug that || causes for numeric and boolean options, and note that both operators short-circuit their right operand.
Show judgment about where each belongs: numeric and boolean settings want ??, while free-text fields where empty means missing may legitimately want || or an explicit emptiness check. Be ready to justify a migration rather than a blanket replace.
Own the API-design angle: decide whether absent options are represented by undefined, null, or a sentinel, and whether defaults are resolved at the boundary once or re-derived at every call site. That choice, not the operator, is what keeps defaults consistent across a system.
## The distinction in one line `||` asks "is the left side falsy?". `??` asks "is the left side null or undefined?". Everything else follows from that. ## The two value sets JavaScript has exactly eight falsy values: `false`, `0`, `-0`, `0n` (the BigInt zero), `""`, `null`, `undefined` and `NaN`. Every other value — including `[]`, `{}`, `"0"` and `"false"` — is truthy. "Nullish" is a much smaller set: only `null` and `undefined`. Nothing else qualifies. `NaN` is not nullish. An empty string is not nullish. `false` is not nullish. So the values where the two operators disagree are precisely the falsy-but-not-nullish ones: `false`, `0`, `-0`, `0n`, `""` and `NaN`. ```js 0 || 50 // 50 0 ?? 50 // 0 '' || 'n/a' // 'n/a' '' ?? 'n/a' // '' false || true // true false ?? true // false NaN ?? 1 // NaN <- ?? does NOT rescue NaN null ?? 1 // 1 undefined ?? 1 // 1 ``` ## Why `||` was the historical idiom Before ES2020 there was no nullish test, so `const port = options.port || 3000` was the standard way to express "use the caller's value if they gave me one". It reads well and works fine as long as no legitimate value is falsy. The moment a legitimate value *is* falsy — a retry count of `0`, a `verbose: false` flag, an empty search string, a slider at zero — the default silently overrides the caller's explicit choice. These bugs are quiet: nothing throws, the program just behaves as if the setting was never provided. ```js function connect(opts = {}) { const retries = opts.retries || 3; // retries: 0 becomes 3 const debug = opts.debug || true; // ALWAYS true — the flag is unusable return { retries, debug }; } connect({ retries: 0, debug: false }); // { retries: 3, debug: true } ``` With `??` both values survive: ```js const retries = opts.retries ?? 3; // 0 stays 0 const debug = opts.debug ?? true; // false stays false ``` ## Short-circuit evaluation Both operators are short-circuiting: the right operand is an expression that is only evaluated if it is needed. `cached ?? computeExpensiveDefault()` does not call the function when `cached` is `0`, and `flag || warn()` does not call `warn` when `flag` is truthy. That matters whenever the right side has side effects or is costly. ## Distinguishing "absent" from "present but empty" The deeper point is that `??` lets you distinguish *missing* from *set to a falsy value*, which `||` cannot do. `undefined` typically means "the key was never provided" (a missing object property, a missing argument, an out-of-range array index). `null` usually means "explicitly set to no value". `??` treats both as absent and everything else as present. That is not always the rule you want. For a text input, an empty string usually *should* be treated as "the user typed nothing", so `||` — or better, an explicit check — is the correct semantic there. `??` is not a blanket replacement for `||`; it is the right tool when falsy-but-meaningful values exist. ## Relationship to default parameters Default parameter values and destructuring defaults use a third rule again: they trigger on `undefined` only, never on `null`. ```js function f(x = 5) { return x; } f(undefined); // 5 f(null); // null <- default does not apply const { a = 1 } = { a: null }; // a === null ``` So `??` sits between `||` (any falsy) and parameter defaults (`undefined` only). ## Syntax constraint `??` may not be mixed with `&&` or `||` in the same expression without parentheses — `a ?? b || c` is a SyntaxError, and you must write `(a ?? b) || c` or `a ?? (b || c)`. The language forces you to state which grouping you meant rather than guessing. ## What to say in an interview State the rule (nullish vs falsy), name the divergent values (`0`, `""`, `false`, `NaN`), give the classic bug (`count || 10` eating a deliberate zero), and add the judgment: `??` is the better default for numeric and boolean options, while an empty string often genuinely means "missing" and deserves an explicit check.
- Does `??` rescue `NaN` the way it rescues `null`?No. `NaN` is falsy but not nullish, so `NaN ?? 1` evaluates to `NaN`. If a parse result may be `NaN` you need an explicit guard, such as `Number.isNaN(n) ? fallback : n`, because neither `??` nor a default parameter will catch it.
- How do default parameter values compare to `??`?A default parameter or destructuring default applies only when the value is `undefined`; `null` is passed through unchanged. `??` treats `null` and `undefined` alike. So `function f(x = 5)` called with `null` returns `null`, whereas `x ?? 5` would return `5`.
- Why is `a?.b ?? fallback` such a common pairing?Optional chaining yields `undefined` when any link in the chain is nullish, and `undefined` is exactly what `??` reacts to. So `user?.prefs?.theme ?? 'dark'` reads as "drill in if you can, otherwise use the default" without swallowing a legitimately empty or zero value found at the end of the chain.
|| is a bouncer who turns away anyone who looks unenthusiastic; ?? only turns away people who never showed up.
saying these in an interview costs you the question
- Says ?? and || are interchangeable, just newer syntax
- Claims ?? falls back on any falsy value including 0
- Thinks NaN is nullish so ?? replaces it
- Believes ?? evaluates its right operand eagerly
- Applies ?? everywhere without checking empty-string cases