The short version
Three ideas. Everything below explains them, with things to try.
In JavaScript, $19.99 × 100 comes out as 1998.9999… Keep money in whole cents.
Your cart, payment provider and ERP can each round correctly, and still disagree.
$10 off three items is $3.33 each, $9.99 in total. Give the spare cent to one line, on purpose.

Two right answers, one cent apart
Three items, $19.99, $24.99 and $9.99, with 20% off everything.
The cart takes 20% off the total: $10.99. The order lines take 20% off each item and add them up: $11.00. Both are correct. They just round in different places.
Nobody complains about a cent. That is the problem: finance finds it at month end, as books that won’t balance and refunds that fail.
Try itChange the prices and watch the two totals split
Computers can’t count cents
By default, most programming languages store decimal numbers in binary, and binary can’t hold 0.1 exactly, just as decimal can’t hold ⅓.
So in JavaScript 0.1 + 0.2 is 0.30000000000000004. One easy mistake when turning $19.99 into cents is to multiply by 100 and drop the decimals: the answer is 1998 cents, not 1999.
Try itRun real JavaScript, and test a price of your own
Go deeper: the fix, and how to spot the bug
Keep money as a whole number of the smallest unit (cents, yen, fils), or in a true decimal type such as BigDecimal in Java, decimal in Python and C#, or DECIMAL in SQL. Payment APIs already work this way: Stripe takes every amount as a whole number in the currency’s smallest unit.5
Signs to look for as a tester:
- a price that saves one cent lower than you typed it;
- an import where
4.35arrives as4.34; - an API response containing
114.99999999999999(that is1.15 * 100); - a total that is right in the cart but a cent off in the email.

Rounding is a choice
Even with exact numbers, someone has to decide what happens to half a cent.
School says round half up. Python and C# round half to the nearest even digit by default, often called banker’s rounding. JavaScript rounds −2.5 to −2.
Each rule is fine on its own. The bug is two systems in the same order flow using different rules.
Try itRound one number with four different rules
Go deeper: who rounds 2.5 which way
| Where | Function | 2.5 | −2.5 |
|---|---|---|---|
| Excel | ROUND(x, 0) | 3 | −3 |
| Ruby | x.round | 3 | −3 |
| PostgreSQL | round(numeric) | 3 | −3 |
| MySQL | ROUND() on exact values | 3 | −3 |
| Python | round(x) | 2 | −2 |
| C# | Math.Round(x) | 2 | −2 |
| JavaScript | Math.round(x) | 3 | −2 |
| Java | Math.round(x) | 3 | −2 |
Binary storage makes it worse: 2.675 is stored as 2.67499999999999982236…, so (2.675).toFixed(2) gives 2.67 when the number you typed rounds to 2.68.

Splitting a discount loses cents
$10 off three $12.50 items is $3.333… each. Round each one and you have $3.33 three times: $9.99.
It bites at refund time. Refund each item on its own and the refunds add up to more than the customer paid, so a payment provider will usually refuse the last one.
The fix: round each share down, then give the leftover cent to one line, on purpose. The pieces then always add up to the whole.
Try itSplit a discount three ways, then refund every line
Go deeper: the three ways systems split, and other currencies
- Round each line. Simple, and the pieces drift from the whole.
- Plug the last line. The last line takes whatever is left. The sum is right, but one product carries everyone’s rounding.
- Largest remainder. Round each down, then hand the spare cents to the lines that lost the most. The sum is right and no line moves by more than a cent. It is the same method used to share out seats in some parliaments.7
Currencies differ too: the Japanese yen has no smaller unit and the Kuwaiti dinar has three decimals.6 A store that assumes two decimals shows “¥333.33”, which is not an amount anyone can pay.
Tax rounds it all again
Tax can be worked out on every line and added up, or once on the whole order.
At New York City’s 8.875%, $19.99 + $4.99 + $7.49 comes to $2.87 one way and $2.88 the other. Which is right depends on the tax rules where you sell. The bug is the storefront and the ERP choosing differently.
Try itSame cart, same rate, two tax totals

A cent, ten thousand times
One cent sounds like nothing. So here are 10,000 simulated orders.
The cart and the ERP each worked correctly. They only rounded in different places. About one order in three ended with them disagreeing, by a cent or two each: too small to report, too common to ignore.
Try itRun the 10,000 orders yourself

The fix: four rules to test
Every money bug above breaks one of these. Put them in the test plan before the first test case.
- The parts add upLines add up to the order total, discount and tax. Exactly, not “within a cent”.
- Every system agreesCart, email, invoice, payment and ERP show the same number for the same order.
- Money comes back exactlyRefunds, full or partial, add up to what was charged.
- The currency sets the decimalsNothing assumes two decimal places.
Open the 12-case test kit
Go deeper: questions to ask your team before testing
- Is money ever stored as a floating-point number, anywhere, including the data passed between systems?
- Which rounding rule do we use, and is it the same in the storefront, payment provider and ERP?
- When a discount is split across lines, which line gets the leftover cent?
- Is tax worked out per line or per order, and who decides?
- Which currencies do we sell in, and where have we assumed two decimals?
- How does a refund work out its amount: from the line, or from what is left on the payment?
The one thing to remember: rounding is a decision. Find out who made it, write it down, and test every system against it.
Sources
- MDN: Math.round() and Number.prototype.toFixed()
- Python: round() and Floating-point arithmetic: issues and limitations
- Microsoft: ROUND function (Excel) and Math.Round (.NET)
- PostgreSQL: mathematical functions, MySQL: rounding behaviour and Ruby: Float#round
- Stripe: supported currencies and zero-decimal currencies
- ISO 4217 currency codes and minor units
- Largest remainders method
The 10,000 orders are simulated in your browser with store-style prices; every figure on this page is checked by automated tests in this site’s code.