DEV Community

Steven Browning
Steven Browning

Posted on AI-assisted

Nine bugs, one cause: my calculators all thought zero meant nothing

I spent two days checking the arithmetic on fifteen of my calculator pages. The audit turned up 34 defects. Nine came back to the same mistake: treating zero as if the user had left something blank.

What bothered me was how normal the code looked. These were separate tools, built at different times, and I had repeated the same assumption across them.

Here is one example:

const inflationRate = parseFloat(document.getElementById('inflationRate').value) || 3;
Enter fullscreen mode Exit fullscreen mode

Empty field? Use 3%. Seems reasonable.

Enter 0? Still 3%.

The calculator quietly replaced the number the person actually entered. No warning, no error, just a different calculation.

Where zero went missing

These are examples from the browser audit, using the pages' own fields and buttons.

Retirement calculator: entering 0 for inflation still applied 3%. With the other inputs unchanged, the target stayed at $1,884,400 instead of $900,000. If someone wants to compare a scenario without inflation, zero needs to stay zero.

EV vs. petrol calculator: entering 0 for electricity still used 14¢/kWh. In the test case, that produced $480 a year in charging costs instead of $0. Free workplace charging is a perfectly reasonable scenario to compare. The calculator should allow it.

Credit card payoff: entering 0% APR cleared the results without explaining why. This was the check:

if (!balance || !apr || !dueDateStr)
Enter fullscreen mode Exit fullscreen mode

Zero APR failed the same check as a missing APR. That rules out an ordinary use case: a promotional 0% offer. The offer's duration needs its own handling, but zero itself is still a valid rate.

GPU mining profitability: this one was in the output logic:

const breakEvenDays = profitDay > 0 ? Math.ceil(gpuCost / profitDay) : null;

if (breakEvenDays) {
  // Show the breakeven time.
} else {
  document.getElementById('res-breakeven').textContent = 'Never';
}
Enter fullscreen mode Exit fullscreen mode

With no new hardware cost and positive daily profit, the result is 0 days. But if (0) is false, so the page showed "Never / Not profitable at this rate" alongside +$0.27/day.

It should distinguish zero days from no breakeven. For this model, that means checking breakEvenDays !== null, rather than whether the number is truthy.

Two GPU comparison tables: a $0 budget showed every card because || Infinity turned zero into an unlimited budget. One table also selected a default GPU when the filtered list was empty. It managed to show "0 cards" beside a named recommendation.

Those are different screens, but the question I had failed to ask was the same: does zero mean something here?

Why I missed it

I had checked empty fields, typical values and very large values. I had not consistently tested zero as its own case.

Looking at the code did not make the problem obvious either. A fallback beside an optional field looks helpful. A truthiness check looks like basic validation. And a number type alone cannot tell you whether zero is allowed for a particular field.

I also relied too much on attributes such as min="0" and max="100". They define validation constraints; they do not automatically stop a custom calculation handler from reading an invalid value. The page still needs to invoke validation or check the inputs itself. MDN explains how number-input validation works.

Blank, zero and invalid need separate decisions

I would not replace every || just because of this audit.

On a travel budget page, a blank optional cost might intentionally mean zero:

const flights = parseFloat(document.getElementById('flights').value) || 0;
Enter fullscreen mode Exit fullscreen mode

That does preserve a typed zero, because the fallback is also zero. It can still hide invalid input, though, so it is not a complete validation strategy.

On a fuel calculator, zero miles per gallon is a different problem: MPG is a divisor. The page should reject it with an explanation, rather than silently replace it with 30.

For an optional numeric field, I would make those choices explicit:

function readOptionalNumber(raw, fallback) {
  const text = raw.trim();
  if (text === '') return fallback;

  const value = Number(text);
  if (!Number.isFinite(value)) {
    throw new Error('Enter a valid number.');
  }

  return value;
}

const inflationRate = readOptionalNumber(
  document.getElementById('inflationRate').value,
  3
);
Enter fullscreen mode Exit fullscreen mode

The caller still needs to show the error beside the field and check any allowed range. This helper only separates blank, zero and non-finite input.

And parseFloat(raw) ?? 3 is not a drop-in fix. An empty string produces NaN, and ?? only falls back for null or undefined. These are separate behaviors: parsing a number and nullish coalescing.

Another way to lose a number: formatting

The percentage calculator had a separate issue. It displayed:

0.01% of 1 = 0
Enter fullscreen mode Exit fullscreen mode

The answer is 0.0001. This time, a falsy check was not responsible. The formatter was:

function fmt(n) {
  const rounded = Math.round(n * 10000) / 10000;
  return parseFloat(rounded.toFixed(4)).toLocaleString();
}
Enter fullscreen mode Exit fullscreen mode

It rounded to four decimal places, then handed the number to a formatter that normally displays at most three fractional digits for plain numbers. The fourth decimal disappeared. The default is documented here.

If four decimal places are the intended display limit, say so:

function fmt(n) {
  return n.toLocaleString(undefined, {
    maximumFractionDigits: 4
  });
}
Enter fullscreen mode Exit fullscreen mode

That fixes this example. It still rounds smaller values, so a tool that needs to show those should use more precision or scientific notation. The displayed answer deserves a test too.

Then there was the opposite problem

The maths page checked for an exact zero before calculating tangent:

const tanV = Math.cos(rad) !== 0 ? r(Math.tan(rad)) : 'undefined';
Enter fullscreen mode Exit fullscreen mode

At 90 degrees, JavaScript's cosine calculation returns approximately 6.123233995736766e-17, not exactly zero. The guard passed, and the page displayed:

cos = 0
tan = 16331239353195370
Enter fullscreen mode Exit fullscreen mode

Underneath was a note explaining that tangent is undefined at 90° and 270°. The explanation was right; the result above it was wrong.

A tolerance can help a degree-based display handle that boundary:

const nearPole = Math.abs(Math.cos(rad)) < 1e-12;
const tanV = nearPole ? 'undefined' : r(Math.tan(rad));
Enter fullscreen mode Exit fullscreen mode

That threshold is a display choice, not a universal floating-point fix. It also catches angles very close to a pole where tangent is large but defined. Choose the rule for the tool's accepted inputs and intended precision, then test both the boundary and nearby values. Tolerance depends on scale and input accuracy.

The audit needed checking as well

Two early findings turned out to be mistakes in how I tested the pages. I called an internal function directly and skipped a guard that ran when a visitor clicked the button.

That changed how I checked the remaining issues: enter the values, click the actual button, and inspect what the visitor sees. A suspicious line of code is a reason to investigate, not proof that the live page is broken.

Checks on the fixes helped too. One caught a hardcoded year I had updated in one function but missed in another. A few checks were wrong themselves and needed correcting. Useful checks need review just like the code they test.

What I would check first now

For each numeric field:

  • Leave it blank.
  • Enter zero.
  • Enter a small positive decimal.
  • Try an invalid or out-of-range value.
  • Check the displayed result, not just the value inside the function.

The frustrating part was that these pages still looked fine. They loaded, the layouts worked, and the numbers were neatly formatted. That made the wrong answers easy to trust.

I build free calculators at StashGrid.site. After this audit, zero gets its own test case.

If you have a calculator or numeric form, try zero in the fields where it is allowed. Does it stay zero, trigger a useful message, or quietly become something else?

Top comments (0)