skip to content

How should you compare two computed JavaScript numbers for approximate equality, and what does `Number.EPSILON` actually represent?

level: middleimportance: must knowfreq 58%

answer

  1. it is a gap, not a floor
  2. spacing of doubles near one
  3. doubles are spaced logarithmically
  4. scale the tolerance by magnitude
  5. relative check, plus absolute floor

basics

~20 s

Number.EPSILON is the gap between 1 and the next representable double, about 2.22e-16 — a relative spacing near 1, not a universal error bound. Compare with a tolerance scaled to the operands' magnitude, not a fixed epsilon.

solid answer

~50 s

`Number.EPSILON` is 2^-52, roughly `2.22e-16`: the distance from 1 to the next double you can represent. It measures the *relative* spacing of doubles near 1, so the popular `Math.abs(a - b) < Number.EPSILON` check is only valid when the values are around that magnitude. For larger values it is far too strict — the gap between neighbouring doubles at a million is about 1e-10, so two results that differ by one rounding step fail the test. Near zero it is far too loose: `1e-18` and `2e-18` differ by a factor of two yet pass. The general form is relative: `Math.abs(a - b) <= tol * Math.max(Math.abs(a), Math.abs(b))`, with `tol` a small multiple of `Number.EPSILON` sized to how many operations the error passed through, plus a separate absolute floor when either side can be zero. For exact domains, don't tune a tolerance — use integers.

code

javascript · 14 lines
javascript
function nearlyEqual(a, b, tol = Number.EPSILON, absFloor = 0) {
  if (a === b) return true;
  const diff = Math.abs(a - b);
  const scale = Math.max(Math.abs(a), Math.abs(b));
  return diff <= Math.max(absFloor, tol * scale);
}

const a = 0.1 * 3 * 1e6; // 300000.00000000006
const b = 0.3 * 1e6;     // 300000

console.log(Math.abs(a - b) < Number.EPSILON); // false - too strict at scale
console.log(nearlyEqual(a, b));                // true
console.log(Math.abs(1e-18 - 2e-18) < Number.EPSILON); // true - too loose near zero
console.log(nearlyEqual(1e-18, 2e-18));                // false

go deeper

for a junior

Know that Number.EPSILON is about 2.22e-16 and that computed floats should be compared with a tolerance rather than ===. Recognise the Math.abs(a - b) < Number.EPSILON idiom when you see it.

for a middle

Explain that epsilon is the gap between 1 and the next double, that double spacing grows with magnitude, and write the relative comparison that scales the tolerance by Math.max of the absolute operands.

for a senior

Demonstrate judgment about the tolerance itself: how many roundings the value passed through, where cancellation inflates relative error, when a domain tolerance in real units is the honest choice, and when the right move is to stop comparing floats altogether.

for a principal

Own it as a system rule: which subsystems are exact by representation and which are approximate with a documented tolerance, so tolerances are not invented per test file, and comparison semantics stay consistent across service boundaries and stored data.

## What Number.EPSILON actually is `Number.EPSILON` is a constant equal to 2^-52, approximately `2.220446049250313e-16`. Its definition is precise and narrow: it is the difference between 1 and the smallest representable double greater than 1. It is *not* the smallest positive number (that is `Number.MIN_VALUE`, about `5e-324`), and it is *not* a bound on the error of an arbitrary computation. The reason it matters is that doubles are spaced *logarithmically*. A double has 53 significant bits, so between any power of two and the next there are 2^52 evenly spaced values. Near 1 the gap between neighbours is about 2.2e-16. Near 1,000,000 the gap is roughly a million times bigger, about 1.2e-10. Near 1e-18 the gap is minuscule. `Number.EPSILON` describes the spacing at magnitude 1 and nowhere else. ## The check everybody writes first ```js Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON; // true ``` This works, and it is where the folklore comes from: the operands are close to 1, so one or two rounding steps of error stays under the epsilon. Generalising it is where the trouble starts. ## Failure one: too strict at scale ```js const a = 0.1 * 3 * 1e6; // 300000.00000000006 const b = 0.3 * 1e6; // 300000 Math.abs(a - b); // 5.82e-11 Math.abs(a - b) < Number.EPSILON; // false ``` The two values differ by a fraction of a rounding step at that magnitude — about 1.9e-16 *relatively*, which is as close as doubles get — yet the absolute check rejects them, because 5.82e-11 dwarfs 2.22e-16. Any tolerance fixed in absolute terms silently becomes an exact-equality test as the numbers grow. ## Failure two: too loose near zero ```js Math.abs(1e-18 - 2e-18) < Number.EPSILON; // true ``` Here the two values differ by 100 percent, and the check calls them equal. Around zero, `Number.EPSILON` is an enormous tolerance: everything smaller than it collapses into one bucket. A physics or statistics routine that tests residuals this way will report convergence it has not achieved. ## The relative comparison The robust form scales the tolerance by the size of the operands: ```js function nearlyEqual(a, b, tol = Number.EPSILON) { if (a === b) return true; // exact, including both zero const diff = Math.abs(a - b); const scale = Math.max(Math.abs(a), Math.abs(b)); return diff <= tol * scale; } ``` With `tol = Number.EPSILON` this asks "are these within one rounding step of each other at their own magnitude", which is the strictest meaningful approximate test. It gives the right answer for both failure cases above. ## Choosing the tolerance One epsilon is right only for a value that has been through about one rounding. Error accumulates: a chain of *n* operations can drift by roughly *n* rounding steps in the worst case, and an ill-conditioned computation (subtracting two nearly equal large numbers — catastrophic cancellation) can lose far more. So `tol` is a judgment call driven by the computation, commonly a modest multiple such as `4 * Number.EPSILON` or `128 * Number.EPSILON` for a long pipeline. State the multiple and why, rather than pasting a magic number. A purely relative test has one hole: when the true answer is zero, the scale is zero and only exact equality passes. Numerical code therefore usually carries an absolute floor as well: ```js const ok = diff <= Math.max(absFloor, tol * scale); ``` where `absFloor` comes from the domain — the smallest difference that is meaningful in the units you are working in. ## Domain tolerance beats numeric tolerance Often the honest tolerance has nothing to do with floating point. If you are comparing distances in metres and half a millimetre is indistinguishable, `Math.abs(a - b) < 0.0005` says what you mean and is far easier to defend in review than a scaled epsilon. Reserve epsilon-based tests for asking "did these two computations produce the same value up to rounding". ## When not to compare at all If the domain is exact — money, counts, quantities with a fixed number of decimal places — an approximate comparison is treating a symptom. Represent the values as integers in the smallest unit and compare them with `===`. A tolerance test on money is a bug waiting to be argued about: is a one-cent difference equal or not? Integers make the question disappear. Finally, a tolerance check is not a valid equivalence relation: it is not transitive, since a can be near b and b near c while a and c are not. That rules it out as a key comparison for sorting, deduplication or map lookup, which is another reason exact domains should never rely on it.

  • How is Number.EPSILON different from Number.MIN_VALUE?
    `Number.EPSILON` is 2^-52, about 2.22e-16 — the gap between 1 and the next double, describing precision. `Number.MIN_VALUE` is about 5e-324 — the smallest positive denormal double, describing range. They answer different questions: how finely doubles are spaced near 1 versus how close to zero a value can get before underflowing.
  • Why is a tolerance-based equality test unsuitable as a Map key or sort comparator?
    Because it is not transitive: a can be within tolerance of b and b within tolerance of c while a and c are not. Equivalence relations and consistent orderings require transitivity, so tolerance comparison produces order-dependent grouping and unstable sorts. Exact domains should compare integers instead.
  • How would you pick the multiplier for the tolerance in a real numerical routine?
    From the computation's length and conditioning: roughly one rounding step per operation in the worst case, more where nearly equal values are subtracted and cancellation amplifies relative error. Pick a stated multiple such as 4 or 128 epsilons, document the reasoning, and pin it with tests over representative inputs rather than tuning until a failing assertion passes.

saying these in an interview costs you the question

  • Calls Number.EPSILON the smallest representable number
  • Uses a fixed epsilon regardless of the operands' magnitude
  • Thinks epsilon bounds the error of any computation
  • Uses a tolerance comparison for monetary amounts
  • Assumes approximate equality is transitive

context