Three rounding bugs I shipped in a unit-conversion calculator, and what finally fixed them

I spent a while building a small calculator that converts between a concentration, a volume and a dose. It is the kind of thing that looks like a one-liner and then quietly produces wrong numbers for months. Three bugs in particular were worth the scar tissue, and none of them are specific to my domain - they show up in any app that converts units and then shows the result to a human.

1. Rounding the display value and then computing from it

The first version formatted the intermediate result for display and then parsed that string back for the next step. A concentration of 2.5 mg/mL became “2.5”, fine, but 0.8333 mg/mL became “0.83” and every downstream number inherited the error. It only showed up when a user chained two conversions.

The fix is boring and absolute: keep one canonical value in base units for the whole calculation, and format only at the very edge, in the render. Never parse your own output. If you find yourself calling parseFloat on something you just called toFixed on, that is the bug.

2. Assuming the user’s unit and the stored unit are the same thing

I stored everything in milligrams and let the UI show micrograms where that read better. Then a field started accepting user input in the displayed unit and writing it straight to storage. Off by a thousand, silently, because both numbers are plausible.

What fixed it was making the unit part of the value rather than a fact about the screen. A { value, unit } pair, with conversion at exactly two boundaries - parse on input, format on output - meant a bare number could no longer travel anywhere. In TypeScript you can go further and brand the type so Milligrams and Micrograms are not assignable to each other. The compiler then catches the class of bug entirely.

3. Rounding to a precision the physical world does not have

The calculator would happily tell someone to draw 0.1873 mL. No syringe has that graduation. The number was mathematically right and practically useless.

The real requirement was to round to the nearest usable increment of the actual instrument, and to say so in the output rather than pretending the exact value was achievable. That turned out to be a general lesson: the correct precision is a property of the thing the user will do with the number, not of the float.

A related trap in the same area - Math.round on the halfway case rounds toward positive infinity in JavaScript, so Math.round(-2.5) is -2, not -3. If a value can be negative and you are rounding for symmetry, that asymmetry will find you.

The check that would have caught all three

Property-based testing, which I had dismissed as academic. The single property “converting a value to another unit and back returns the original within epsilon” would have caught bugs 1 and 2 on the first run, across thousands of generated inputs, without me having to imagine the failing case. Roundtrip properties are almost free to write and they are unreasonably effective on conversion code.

If you are working on anything that converts units - currency, distance, timezones, storage sizes - the summary is: one canonical representation, unit attached to the value, format only at the boundary, and round to what the user can actually do.

This is interesting, but a bit over my head. What does a code example of property testing look like?