Round 1.005 to two decimals and you can get 1.00, 1.01 or 1 depending on which JavaScript tool you reach for. I ran the common options in Node v22.23.4 (V8) on a set of "half-cent" values and compared them with the exact stored value of each number.
Setup
For each value: toFixed(2), Math.round(v * 100) / 100, Intl.NumberFormat("en-US", {minimumFractionDigits: 2, maximumFractionDigits: 2}) with the default rounding, the same with roundingMode: "halfEven", and v.toPrecision(20) to see what the double really holds.
| Value | exact stored value | toFixed(2) | Math.round(v*100)/100 | Intl default | Intl halfEven |
|---|---|---|---|---|---|
| 1.005 | 1.00499999999999989 | 1.00 | 1 | 1.01 | 1.00 |
| 2.675 | 2.67499999999999982 | 2.67 | 2.68 | 2.68 | 2.68 |
| 8.345 | 8.34500000000000064 | 8.35 | 8.35 | 8.35 | 8.34 |
| 1.255 | 1.25499999999999989 | 1.25 | 1.25 | 1.26 | 1.26 |
| 0.615 | 0.61499999999999999 | 0.61 | 0.62 | 0.62 | 0.62 |
| 10.235 | 10.23499999999999943 | 10.23 | 10.24 | 10.24 | 10.24 |
| -1.005 | -1.00499999999999989 | -1.00 | -1 | -1.01 | -1.00 |
What I see
The three methods disagree on the same input. For 1.005, toFixed follows the stored value (just under 1.005) and gives 1.00. Math.round(v * 100) gives 1 because 1.005 * 100 is 100.49999999999999. Intl.NumberFormat gave 1.01, which is what a person expects, even though the stored value is below the half. I did not look into why. I only report that it differs from toFixed.
Math.round is not the same as the others either. For 2.675 and 0.615 it rounds up while toFixed rounds down, because the multiplication v * 100 lands on a different double than the one you started with.
Rounding mode changes the answer even when the number is exact. 8.345 is stored just above the half, so halfEven gave 8.34 while default gave 8.35. The whole numbers show it more clearly: Math.round(2.5) is 3 and halfEven gives 2. Math.round(-0.5) returns -0, while Intl default gave -1.
MDN documents the rest: toFixed can look wrong because decimal fractions are not exact in floating point, and Intl.NumberFormat rounds half away from zero by default (halfExpand), with halfEven and other modes available through roundingMode.
What I would do for money
- Keep amounts as integer cents, and round once, at the end, with a rule you write down.
- Pick the rule on purpose: half away from zero (what most people expect) or half even (what some ledgers use).
- Use
Intl.NumberFormatfor display only. Do not parse its output back into a number to do maths. - Add test rows for 1.005, 2.675 and a negative value. They are the ones that split the methods.
Not tested
Browsers (Node only), other locales, currencies with different digit counts, and any money library. Check your target runtime, since roundingMode needs a recent engine.
Top comments (0)