unrust

Keep zero: choosing between || and ?? in JavaScript

Published

By the Unrust team · 6 min read · Editorial method

Follow a zero-retry setting through logical OR and nullish coalescing. Compare false, empty strings, null and undefined before choosing a default.

Open the free companion tool →

Decide what missing means in your program

Suppose a notification job accepts a retry limit. The caller can set 0 to mean no retries, or omit the setting to accept a default of 3. Those are different instructions. Before choosing a defaulting operator, write that distinction into the contract: zero is present and meaningful; null or undefined means use the default.

Now review the two expressions below. Logical OR replaces a falsy left operand, which includes zero. Nullish coalescing replaces only null or undefined. For this contract, ?? preserves the caller’s explicit zero while || silently turns it into three retries. The shorter-looking expression is not necessarily the one that preserves intent.

const options = { retries: 0 };
const broadDefault = options.retries || 3;  // 3
const missingDefault = options.retries ?? 3; // 0

Follow the value through both operators

Open the truthiness explorer and choose 0. Boolean(0) is false. The || expression therefore chooses the fallback, while ?? keeps 0. Next choose false, then the empty string. Both are falsy, but neither is nullish, so the operators disagree in the same way.

Choose null and undefined next. Both operators use the fallback in those cases. The fallback in the explorer is deliberately a visible string, so you can distinguish it from the original value at a glance. In your own program it may be a number, object or function result. Neither operator promises a Boolean result: each returns one of its operands.

Do not use truthiness as an emptiness test

Select an empty array or an empty object. Both are truthy, so both operators keep the original value. If you need to know whether a list has entries, check its length. If you need a property, check the property according to the data contract. A truthiness check does not inspect the contents of a collection.

Then select the string "0". It is a non-empty string and therefore truthy. A numeric zero and text containing a zero do not behave the same way. This often matters when values come from form controls or query parameters: first decide how to parse and validate the input, then decide how a missing value should default. Avoid relying on loose equality to explain truthiness; they are different operations.

A default does not validate the supplied value

NaN is falsy but not nullish. The explorer shows || replacing it and ?? preserving it. Neither behavior establishes that the value is a valid retry limit. Negative numbers, fractional numbers and strings also need their own policy. Choose the default for an absent value, then validate the resulting candidate against the actual requirement.

For example, this function accepts only nonnegative integers after applying the missing-value default. The exact error handling belongs to the surrounding application; the example throws so an invalid value cannot quietly become an approved configuration.

function retryLimit(value) {
  const candidate = value ?? 3;
  if (!Number.isInteger(candidate) || candidate < 0) {
    throw new Error("Retry limit must be a nonnegative integer");
  }
  return candidate;
}
retryLimit(0);         // 0
retryLimit(undefined); // 3
retryLimit(NaN);       // throws

Choose tests that expose a changed contract

When reviewing a replacement of || with ??, test an omitted value, null, zero, false, an empty string and a normal accepted value. You may expect some of those inputs to fail validation. That expectation is part of the contract, too. A test containing only a positive number cannot tell you which operator is appropriate because both preserve it.

Also check whether evaluating the fallback does work. Both operators short-circuit, but their conditions differ. If the right side calls a function, changing the operator can change whether that function runs. Keep defaults simple when possible, and make side effects explicit enough for a reviewer to notice.

Practice the decision, not the slogan

For a display label, an empty string might intentionally hide text. For a search term, an empty string might mean the query is missing. The same value can therefore need a different policy in two places. Write the policy in one sentence, predict the behavior of each operator, and use the explorer to check your prediction.

The useful habit is preserving meaning while changing code. These examples are open learning material and do not count toward an Unrust score. Use the broader idiomatic-code guide for more practice, or take the free diagnostic separately for a timed snapshot of your unassisted skills.

References and next steps

Try the reasoning without the tool

Explain one fresh example before checking its output. For a broader starting point, take the free diagnostic with tools and hints closed. A free account then gives you a daily practice queue.