TL;DR: Math.sumPrecise() adds up an iterable of numbers without the rounding errors that pile up in a reduce() loop. Same numbers in, same answer out, whatever the order, and it has been Baseline since April 2026.
The total that changed when you sorted the table
Here is a shopping cart:
const cart = [
{ name: "Mechanical keyboard", price: 19.99 },
{ name: "USB-C cable", price: 4.95 },
{ name: "Sticker", price: 0.1 },
{ name: "Mouse pad", price: 12.5 },
{ name: "Sticker", price: 0.2 },
{ name: "Cable ties", price: 7.35 },
{ name: "Sticker", price: 0.3 },
{ name: "Screen wipes", price: 3.15 },
];
const total = (list) => list.reduce((sum, item) => sum + item.price, 0);
const byPrice = cart.toSorted((a, b) => a.price - b.price);
total(cart); // 48.54
total(byPrice); // 48.53999999999999
Same eight items. Someone clicked "sort by price", and the total moved.
Addition is supposed to be commutative. Floating point addition is commutative for two numbers, but a long chain of them is not associative. Every + rounds its result to the nearest 64-bit float, and those little roundings land differently depending on the order.
It gets worse with big and small numbers together:
[1e20, 0.1, -1e20].reduce((a, b) => a + b, 0); // 0
1e20 + 0.1 rounds straight back to 1e20. The 0.1 is gone before -1e20 arrives. Your dime fell into the couch.
Meet Math.sumPrecise()
Math.sumPrecise(numbers)
It takes one iterable of numbers and returns their sum. The spec asks for the exact mathematical sum of the inputs, rounded to a float only once, at the very end. No intermediate rounding, so order can't matter.
Math.sumPrecise([1e20, 0.1, -1e20]); // 0.1
Math.sumPrecise([0.1, 0.2, 0.3]); // 0.6
Math.sumPrecise([0.3, 0.2, 0.1]); // 0.6
Math.sumPrecise(Array(10).fill(0.1)); // 1
Compare that with the loop:
const naive = (a) => a.reduce((x, y) => x + y, 0);
naive([0.1, 0.2, 0.3]); // 0.6000000000000001
naive([0.3, 0.2, 0.1]); // 0.6
naive(Array(10).fill(0.1)); // 0.9999999999999999
naive(Array(1000).fill(0.1)); // 99.9999999999986
Math.sumPrecise(Array(1000).fill(0.1)) gives 100. The loop is off in the 12th digit after only a thousand additions.
Every value above comes from running the code in Chromium 152.
The cart, fixed
const totalPrecise = (list) => Math.sumPrecise(list.map((item) => item.price));
totalPrecise(cart); // 48.54
totalPrecise(byPrice); // 48.54
Sort by price, by name, or shuffle it. The total stays put.
It accepts any iterable, not only arrays, so it slots into code that never builds an array at all:
const scores = new Map([["ana", 0.1], ["ben", 0.2], ["cy", 0.7]]);
Math.sumPrecise(scores.values()); // 1
function* readings() {
yield 0.1;
yield 0.2;
yield 0.7;
}
Math.sumPrecise(readings()); // 1
It also survives overflow in the middle of the sum:
naive([1e308, 1e308, -1e308]); // Infinity
Math.sumPrecise([1e308, 1e308, -1e308]); // 1e+308
The loop overflows to Infinity halfway through and never comes back. sumPrecise only cares where the sum ends up.
And no, it isn't slow. In a quick test in Chromium 152, summing a million random numbers took about 4ms with Math.sumPrecise() and about 16ms with reduce(). That's one machine, so measure your own hot paths.
Can I use it?
In browsers, yes:
- Firefox 137
- Safari 26.2
- Chrome and Edge 147, which made it Baseline Newly available in April 2026
Node.js is the catch. MDN's compat data lists no Node version yet, and typeof Math.sumPrecise is "undefined" in Node 24.11. For code that runs on the server or in older browsers, feature-detect it, or use the core-js or es-shims polyfills that MDN links to.
const sum = (numbers) =>
typeof Math.sumPrecise === "function"
? Math.sumPrecise(numbers)
: [...numbers].reduce((a, b) => a + b, 0);
The fallback is the old, order-sensitive behavior, so don't promise users exact totals on it.
Gotchas
1. It can't fix 0.1 + 0.2. Math.sumPrecise([0.1, 0.2]) is still 0.30000000000000004. The literal 0.1 was never exactly one tenth to begin with, and that float sum rounds to the same answer either way. sumPrecise removes the error from adding, not the error from storing.
2. Strings are a TypeError, not a coercion. Unlike Math.max(), it refuses anything that isn't a number:
Math.sumPrecise(["1", 2]);
// TypeError: Type of 'Iterator value' must be 'number', found 'string'
Values from form inputs, CSV files, or dataset attributes need Number() first. null and BigInt get the same treatment. Honestly, that's a feature.
3. It wants one iterable, not spread arguments. Math.sumPrecise(1, 2, 3) throws TypeError: [object Number] is not iterable. Muscle memory from Math.max(...list) will bite here. The proposal chose this on purpose: spreading a huge list into arguments can blow the stack.
4. The empty sum is -0. Object.is(Math.sumPrecise([]), -0) is true. It prints as 0 and compares equal to 0, so you'll rarely notice, until something checks with Object.is().
5. Infinity and NaN behave like normal math. [Infinity, 1] sums to Infinity, while [Infinity, -Infinity] and anything containing NaN give NaN.
6. Money still wants integers. sumPrecise makes float sums correct and stable. If you're storing currency, integer cents (or BigInt) are still the real fix.
Wrap-up
Math.sumPrecise() is the sum you thought reduce() was giving you. Go find the reduce((a, b) => a + b, 0) in your codebase and give it a proper name.
Sources
- MDN: Math.sumPrecise()
- TC39 proposal: Math.sumPrecise
- MDN browser compat data for Math
- web.dev: July 2026 Baseline monthly digest
This article was written with the help of AI and checked against the sources above.
Top comments (0)