DEV Community

Saurabh Sharma
Saurabh Sharma

Posted on

The rounding bug that almost shipped in a payroll calculator

I've been building PayTimeHub, a set of free payroll and pay calculators - paycheck withholding, raise calculator, hours-to-decimal, time and a half. Money math looks trivial until you actually ship it, and one bug almost got through that's worth writing up.

The setup

Every calculator on the site takes a dollar amount, does some arithmetic on it, and shows the result back to the user. A raise calculator multiplies a salary by a percentage. An overtime calculator multiplies an hourly rate by 1.5. Simple stuff, until you remember that JavaScript does floating-point math.

Try this in a console:

0.1 + 0.2
// 0.30000000000000004
Enter fullscreen mode Exit fullscreen mode

Now imagine that showing up on a page that's supposed to tell someone their new salary. A $52,000 salary with a 3.85% raise should read as a clean $54,000. Depending on how you chain the multiplication and division, you can end up with something like $53,999.999999999996, which then gets formatted and displayed as a genuinely wrong number - or worse, rounds inconsistently between two calculators that are supposed to agree with each other.

The fix

Nothing exotic, just a single shared rounding function that every calculator on the site routes its output through before display:

export function roundCurrency(amount: number): number {
  return Math.round((amount + Number.EPSILON) * 100) / 100;
}
Enter fullscreen mode Exit fullscreen mode

The Number.EPSILON nudge matters more than it looks like it should. Without it, plain Math.round(amount * 100) / 100 still misfires on specific inputs, because the multiplication itself can introduce a floating-point error just below a rounding boundary, so a value that should round up rounds down instead. Adding the smallest possible float before rounding pushes those edge cases back onto the correct side without changing any value that wasn't on the boundary to begin with.

Why it's worth writing down

The interesting part isn't the fix - it's that this exact class of bug is invisible in casual testing. Round numbers like $50,000 and 5% never trigger it. It shows up on the specific combinations that don't divide cleanly, the ones a user actually types in, not the ones a developer thinks to test.

Every money-handling calculator on the site now funnels through one rounding function instead of each one doing its own .toFixed(2) inline. Partly for consistency, but mostly so this bug only had one place it could hide.

If you're building anything that does arithmetic on money in JavaScript and displays the result, it's worth checking whether you have one rounding function or five slightly different ones scattered across the codebase. It's an easy thing to get right once, and an easy thing to get subtly wrong five separate times.

Built with Astro, if anyone's curious about the stack. The site itself is at paytimehub.com if you want to see it in action — specifically the pay raise calculator, where this first came up.

Top comments (0)