Take three invoice lines with unrounded amounts of 0.335 each. Round each line to two decimal places, then add them: 0.34 + 0.34 + 0.34 = 1.02. Add the original amounts first and round the result: 1.005 becomes 1.01.
An invoice screen could use the first calculation while its PDF uses the second. Both can use a decimal library and still disagree. The difference is where they round.
These are made-up amounts, using two decimal places and half-up rounding. The rounding policy required for a real invoice depends on the rules that apply to it.
Where the extra cent comes from
| Step | Round each line first | Round the document total |
|---|---|---|
| Input amounts | 0.335, 0.335, 0.335 | 0.335, 0.335, 0.335 |
| Values added | 0.34, 0.34, 0.34 | 0.335, 0.335, 0.335 |
| Sum before the final display | 1.02 | 1.005 |
| Final amount | 1.02 | 1.01 |
big.js can calculate both exactly. It leaves the choice of when to round to your code.
To run the example, install big.js in an empty directory:
npm init -y npm install big.js@7.0.1 Save this as rounding.cjs, then run node rounding.cjs:
const assert = require('node:assert/strict'); const Big = require('big.js'); function totalByLine(amounts, scale) { let total = new Big('0'); for (const amount of amounts) { total = total.plus(new Big(amount).round(scale, Big.roundHalfUp)); } return total.toFixed(scale); } function totalByDocument(amounts, scale) { let total = new Big('0'); for (const amount of amounts) { total = total.plus(amount); } return total.round(scale, Big.roundHalfUp).toFixed(scale); } const lines = ['0.335', '0.335', '0.335']; assert.equal(totalByLine(lines, 2), '1.02'); assert.equal(totalByDocument(lines, 2), '1.01'); const credits = ['-0.335', '-0.335', '-0.335']; assert.equal(totalByLine(credits, 2), '-1.02'); assert.equal(totalByDocument(credits, 2), '-1.01'); assert.equal(totalByDocument(['0.005'], 2), '0.01'); assert.equal(totalByDocument(['-0.005'], 2), '-0.01'); console.log('6 assertions passed'); The functions accept and return decimal strings, avoiding a conversion through JavaScript's binary floating-point Number. The scale argument controls the number of decimal places; two is just the value used here.
Check the negative cases too. In big.js, roundHalfUp rounds a tie away from zero, so -0.005 becomes -0.01. That matters when the same code handles credits or reversals.
Keep the PDF and payment amount in step
With a line-level rounding policy, the saved line totals in this example would be 0.34 each, adding up to 1.02. With a document-level policy, you need enough precision to add the unrounded amounts before producing the final 1.01.
The second approach also leaves a display problem: three visible lines of 0.34 don't add up to 1.01. You might need to show more decimal places or a separate rounding adjustment, depending on the invoice rules you're working under. Silently changing a source line would make the calculation harder to trace.
Once the invoice total is final, the PDF, export, and payment request should all use it. If the PDF rebuilds the total from formatted line amounts, it can introduce the discrepancy again.
Use a fixture that catches it
A test using 10.25 and 20.50 passes with either function. Both return 30.75, so that test won't catch a change in rounding policy.
Keep the fractional-cent example alongside the ordinary amounts, including its negative version and the halfway cases. If you support other currency scales, cover those too. At scale zero, for example, two lines of 0.5 total 2 when rounded separately and 1 when added first.
Then compare the finalized invoice amount with what the PDF, export, and payment request actually use. An assertion allowing a one-cent difference would let this mismatch pass.
References: big.js rounding modes, big.js round, and JavaScript Number representation.
AI disclosure: This article and its example code were generated with an AI coding agent. The runnable example was executed locally with Node.js v24.22.1 and big.js 7.0.1. All amounts are synthetic test fixtures.