skip to content

A service reads its settings with `const retries = options.retries || 3` and `const prefix = options.prefix || 'app'`. A caller passes `{ retries: 0, prefix: '' }` and both values are silently ignored. Explain the mechanism and how you would fix it.

level: seniorimportance: must knowfreq 60%

answer

  1. the operator tests more than presence
  2. zero and empty string are on the falsy list
  3. 'missing' and 'falsy' are not the same question
  4. a nullish-only fallback exists since ES2020
  5. ask per site whether a falsy value is legal

basics

~10 s

|| tests truthiness, and 0 and '' are falsy, so both caller values lose to the defaults. Use ??, which falls back only on null and undefined: options.retries ?? 3 keeps an explicit 0.

solid answer

~50 s

The `||` idiom does not mean "if the option is missing" — it means "if the option is falsy". `0` and `''` are both falsy, so the caller's deliberate values are discarded and replaced by the defaults, with no error anywhere. The fix is the nullish coalescing operator `??` (ES2020), which tests only for `null` and `undefined` and therefore preserves `0`, `''` and `false`: `const retries = options.retries ?? 3`. When I hit this in production I grep for `|| ` on the right-hand side of a default and audit each one against the option's real domain — `||` is still correct where every falsy value is genuinely invalid, such as a display name or a base URL. For a settings object I would also validate the merged config once at the boundary, so a bad `retries` fails loudly rather than being quietly reinterpreted.

code

javascript · 13 lines
javascript
const options = { retries: 0, prefix: '' };

// the bug: || tests truthiness, not presence
console.log(options.retries || 3);   // 3   — the caller's 0 was discarded
console.log(options.prefix || 'app'); // 'app'

// the fix: ?? falls back only on null / undefined
console.log(options.retries ?? 3);   // 0   — preserved
console.log(options.prefix ?? 'app'); // ''  — preserved
console.log(options.timeout ?? 5000); // 5000 — genuinely missing

// ?. covers a missing container as well
console.log(undefined?.retries ?? 3); // 3

go deeper

for a junior

Recognise that || falls back on any falsy value, so a deliberate 0 or '' gets replaced, and know ?? as the operator that falls back only on null and undefined.

for a middle

Explain both operators' tests precisely, including that ?? short-circuits and cannot be mixed with || unparenthesised, and contrast them with destructuring defaults that trigger on undefined alone.

for a senior

Demonstrate the diagnosis: this fails silently with no log or exception, so you confirm it by comparing the caller's value against the value just after the default is applied, then audit every default site against its domain.

for a principal

Argue the systemic fix — defaults scattered across a codebase are unauditable, so centralise config merging and validation at one boundary and design option domains that do not make a falsy value meaningful in the first place.

## The mechanism `a || b` evaluates `a`, applies the abstract ToBoolean operation to it, and returns `a` when that test is `true` and `b` otherwise. ToBoolean's falsy set is `false`, `0`, `-0`, `0n`, `''`, `null`, `undefined` and `NaN`. So the intent the author wrote and the intent they meant diverge: - **meant:** "use 3 when the caller did not supply `retries`" - **written:** "use 3 when `retries` is missing **or** zero **or** NaN" `0` is a legitimate retry count ("do not retry"), `''` is a legitimate prefix ("no prefix"), and `false` is a legitimate flag value. Each is silently upgraded into the default, so disabling a feature turns it on. The bug is invisible in tests written with non-zero fixtures and shows up only when someone finally passes the falsy-but-valid value. ## The fix: `??` Nullish coalescing, added in ES2020, is the operator that means what the author intended: ```js const retries = options.retries ?? 3; // 0 survives const prefix = options.prefix ?? 'app'; // '' survives ``` `a ?? b` returns `b` **only** when `a` is `null` or `undefined`. Every other value — including all the other falsy ones — is returned unchanged. Like `||`, it short-circuits: the right operand is not evaluated when the left is non-nullish, so an expensive default is not computed needlessly. Note also that mixing `??` with `||` or `&&` in one expression without parentheses is a SyntaxError; the grammar forces you to state the grouping. The closely-related tool is optional chaining. `options?.retries ?? 3` handles the case where `options` itself may be absent: `?.` yields `undefined` instead of throwing, and `??` then supplies the default. The pair is the idiomatic "read a possibly-missing nested setting with a default". A third mechanism is worth knowing for contrast: destructuring and parameter defaults fire **only on `undefined`**, never on `null` and never on other falsy values. So `function f({ retries = 3 } = {})` keeps a `0` correctly — but it will *not* substitute the default for an explicit `null`, which is a different fence from `??`'s. ## When `||` is still the right operator This is the part that separates a rote answer from a considered one. `||` is correct whenever the option's valid domain excludes **every** falsy value: ```js const label = user.displayName || 'Anonymous'; // '' should behave as missing const host = cfg.host || 'localhost'; // an empty host is not usable ``` Here the empty string genuinely *is* an absent value, and `||` collapses "missing" and "blank" on purpose — which is usually what you want for human-facing text. Blanket-replacing every `||` with `??` introduces the opposite bug: empty strings from a form now flow through as real values. The decision rule is one question per site: **is any falsy value a legal value for this option?** If yes, `??`. If no, `||` is fine and arguably clearer. ## How to find and prevent it 1. **Grep the defaults.** `\|\| ` immediately before a literal is a strong signal. Review each against the option's domain. 2. **Test with the falsy member.** Any option whose type is a number, a string or a boolean deserves a test case that passes `0`, `''` or `false` explicitly. The bug only exists because nobody wrote that test. 3. **Validate once, at the boundary.** Rather than scattering defaults through the code, merge and validate the config in one place. A schema check turns "silently reinterpreted" into "rejected with a message", which is the real production win — the failure mode here is not the wrong value, it is the *silence*. 4. **Prefer domains without falsy members** when designing the API. If `retries: 0` is confusing, an explicit `retry: false` plus `retries: n` may model the intent better. ## Diagnosing it live Symptomatically this presents as "the setting has no effect". The fastest confirmation is to log the value immediately after the default is applied and compare it against what the caller passed — if the caller's `0` becomes `3` at that line, you have found it. Because no exception is thrown and no log line is emitted, monitoring will never surface it; only a value-level comparison will.

  • Would you replace every `||` default in the codebase with `??`?
    No — that swaps one bug for another. `||` is correct wherever no falsy value is legal, which is common for human-facing strings: `user.displayName || 'Anonymous'` should treat `''` as missing, and `??` would let a blank name through to the UI. I review each default against the option's actual domain and change only the sites where `0`, `''` or `false` is meaningful.
  • How do destructuring defaults compare with `??` for this problem?
    Destructuring and parameter defaults substitute only when the value is `undefined`, so `{ retries = 3 }` correctly preserves `0` — but it also lets an explicit `null` through untouched, where `??` would replace it. Choose by which sentinel your callers actually send: `undefined` from an absent key, `null` from JSON that encodes 'no value'.
  • What makes this class of bug hard to catch before production?
    It fails silently and asymmetrically. Nothing throws, nothing logs, and the code path is exercised constantly with non-falsy fixtures, so coverage looks complete. It only appears when a caller supplies the one falsy member of the domain. The countermeasure is a test per option that passes exactly that value, plus validating the merged config in one place so bad input is rejected rather than reinterpreted.

saying these in an interview costs you the question

  • Says `||` checks whether the property exists
  • Believes `??` is just a shorter spelling of `||`
  • Thinks `0` only fails here because of loose equality with false
  • Replaces every `||` default with `??` without checking domains
  • Claims a linter or type checker would have caught it

context