DEV Community

Cover image for Why Your JavaScript Sums Are Wrong (And How to Fix Them)
Piyush Chauhan
Piyush Chauhan

Posted on

Why Your JavaScript Sums Are Wrong (And How to Fix Them)

You know that Array.prototype.reduce you've used a thousand times to sum numbers? It's lying to you.

1. A One-Liner That Should Work

const nums = Array.from({ length: 1000 }, () => 0.1);
const sum = nums.reduce((a, b) => a + b, 0);

console.log(sum);        // 99.9999999999986
console.log(sum === 100); // false (!)
Enter fullscreen mode Exit fullscreen mode

A thousand tenths should equal one hundred. But in JavaScript — and in virtually every other mainstream language — it doesn't. Not exactly.

If you've ever shrugged off a 0.0000000000001 discrepancy, this article is for you. Because at scale, that "tiny" error isn't tiny at all.


2. The Problem: Why Floats Betray Us

JavaScript uses IEEE 754 double-precision floating-point for all numbers. In plain English: your numbers are stored as binary fractions, and most decimal fractions (like 0.1) have no exact binary representation.

When you add 0.1 + 0.2, the computer isn't adding "one-tenth plus two-tenths." It's adding the closest binary approximations it can store. The result? 0.30000000000000004.

Now multiply that imperfection across a thousand, a million, or a billion additions. The error doesn't stay small — it accumulates.

The Core Issue

When you sum left-to-right with a running total, you eventually reach a point where the running total is huge and the next number is tiny. In floating-point arithmetic, adding a tiny number to a huge number often results in no change at all — the tiny number gets "swallowed."

Imagine pouring water into a bucket with a small hole. Each pour loses a few drops. After 1,000 pours, you're missing a puddle.


3. The Fix: Kahan Summation

In 1965, William Kahan published an algorithm that tracks and recovers the low-order bits lost in each addition. The trick? A compensation variable that remembers what got rounded away.

The Intuition

Instead of just keeping a running total, Kahan summation keeps two values:

  1. sum — the best approximation so far
  2. c (compensation) — the error that was discarded in the last step

Before each new addition, the compensation from the previous step is added back in. After the addition, the new error is computed and stored for the next iteration.

The Algorithm

Input: x
       │
       ▼
    x + c  ◄── compensation from last step
       │
       ▼
   t = sum + (x + c)
       │
       ▼
   c = (t - sum) - (x + c)  ← the "recovered" error
       │
       ▼
   sum = t
Enter fullscreen mode Exit fullscreen mode

JavaScript Implementation

function kahanSum(numbers) {
  let sum = 0;
  let c = 0; // compensation for lost low-order bits

  for (let i = 0; i < numbers.length; i++) {
    const y = numbers[i] - c;  // subtract previous error
    const t = sum + y;         // tentative sum
    c = (t - sum) - y;         // new error: (what we added) - (what we meant to add)
    sum = t;
  }

  return sum;
}
Enter fullscreen mode Exit fullscreen mode

Line-by-Line Breakdown

Line What It Does
y = numbers[i] - c Add back the error from the previous step
t = sum + y Perform the actual addition
c = (t - sum) - y Calculate what got lost in rounding
sum = t Commit the new sum

The magic is in (t - sum) - y. Because of how floating-point subtraction works, this expression recovers the bits that were rounded away during sum + y.


4. Live Comparison: Naive vs. Kahan

Let's run both algorithms on the same pathological dataset:

// 10 million copies of 0.0001
// True answer: exactly 1000
const nums = Array.from({ length: 10_000_000 }, () => 0.0001);

console.time('naive');
const naive = nums.reduce((a, b) => a + b, 0);
console.timeEnd('naive');

console.time('kahan');
const kahan = kahanSum(nums);
console.timeEnd('kahan');

console.log('Naive: ', naive);  // 999.9998999965879
console.log('Kahan: ', kahan);  // 1000.0000000000001
Enter fullscreen mode Exit fullscreen mode

The naive sum is off by 0.0001. That might not sound like much, but in financial systems processing millions of transactions, "small" errors compound into real money.

Kahan's error? 1.1e-13 — effectively zero for all practical purposes.


5. The Middle Ground: Pairwise Summation

Before we crown Kahan the winner, there's a third contender that often gets overlooked: pairwise summation (also called cascade summation).

The idea is beautifully simple: instead of accumulating left-to-right, recursively split the array in half, sum each half, then add the two results. This ensures you're always adding numbers of roughly the same magnitude.

The Intuition

Imagine balancing a scale. Naive summation keeps piling weights on one side until the running total dwarfs every new addition. Pairwise summation weighs equal-sized groups against each other at every step.

[1, 2, 3, 4, 5, 6, 7, 8]
    /              \
[1,2,3,4]      [5,6,7,8]
  /    \          /    \
[1,2] [3,4]    [5,6] [7,8]
 |     |        |     |
 3     7        11    15
   \   /          \  /
    10             26
         \        /
           36
Enter fullscreen mode Exit fullscreen mode

Recursive Implementation

function pairwiseSum(arr, start = 0, end = arr.length) {
  const len = end - start;
  if (len === 0) return 0;
  if (len === 1) return arr[start];

  const mid = start + Math.floor(len / 2);
  return pairwiseSum(arr, start, mid) + pairwiseSum(arr, mid, end);
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Caution: For very large arrays (> ~10,000 items), the recursive version will blow the call stack. Here's a stack-safe iterative version:

🔧 Stack-safe iterative version

function pairwiseSumIterative(arr) {
  if (arr.length === 0) return 0;

  let queue = arr.slice();

  while (queue.length &gt; 1) {
    const next = [];
    for (let i = 0; i &lt; queue.length; i += 2) {
      next.push(queue[i] + (queue[i + 1] || 0));
    }
    queue = next;
  }

  return queue[0];
}
Enter fullscreen mode Exit fullscreen mode

6. The Heavyweight: decimal.js

Sometimes you need exact decimal arithmetic, not just "less error." That's where decimal.js comes in. It represents numbers as strings internally and performs arbitrary-precision math.

Installation

npm install decimal.js
Enter fullscreen mode Exit fullscreen mode

Usage

const Decimal = require('decimal.js');

function decimalSum(numbers) {
  return numbers
    .reduce((sum, n) => sum.plus(new Decimal(n.toString())), new Decimal(0))
    .toNumber();
}
Enter fullscreen mode Exit fullscreen mode

💡 Pro tip: Always pass numbers through .toString() before constructing a Decimal. new Decimal(0.1) still inherits floating-point imprecision because 0.1 is already corrupted by the time it reaches the constructor.


7. The Showdown: Four-Way Comparison

Let's put them all to the test with a pathological dataset:

// 10 million copies of 0.0001
// True answer: exactly 1000
const nums = Array.from({ length: 10_000_000 }, () => 0.0001);
Enter fullscreen mode Exit fullscreen mode
Method Result Error Relative Speed
Naive reduce 999.9998999965879 ~1.0e-4 1.0x (baseline)
Pairwise 999.9999999999123 ~8.8e-11 ~1.5x slower
Kahan 1000.0000000000001 ~1.1e-13 ~3.5x slower
decimal.js 1000 (exact) 0 ~40-80x slower

What the Numbers Tell Us

  • Naive is fast but wrong at scale. The error is small in absolute terms, but in financial systems, "small" errors compound into real money.
  • Pairwise dramatically reduces error with minimal overhead. Its error grows logarithmically O(log n) instead of linearly.
  • Kahan achieves near-constant error O(ε) — the gold standard for floating-point summation.
  • decimal.js is exact but slow. It's not really a summation algorithm; it's a different number system entirely.

8. The Decision Framework

Here's a quick cheat sheet for choosing the right tool:

Scenario Recommendation Why
Small arrays (< 1,000 items) Naive reduce Error is negligible; don't over-engineer
Large arrays, need speed, "good enough" accuracy Pairwise Logarithmic error growth, parallelizable
Financial/scientific computing, must minimize error Kahan Constant error bound, no extra dependencies
Accounting, currency, must be exact decimal.js Arbitrary precision eliminates rounding entirely
GPU/Web Worker parallel processing Pairwise Perfectly parallelizable; Kahan is inherently sequential

The Trade-off Triangle

        Speed
         /\
        /  \
       /    \
      /      \
     /   ⚠️   \
    /__________\
 Accuracy    Simplicity
Enter fullscreen mode Exit fullscreen mode

You can pick two:

  • Fast + Simple → Naive
  • Fast + Accurate → Pairwise
  • Accurate + Simple → Kahan
  • Exact → decimal.js (sacrifices speed)

9. When NOT to Use Kahan

Kahan summation is great, but it's not always the right call:

  1. Integers only — If your array contains only integers representable in 53 bits, naive summation is exact until you exceed Number.MAX_SAFE_INTEGER.
  2. Already using a decimal library — If you've bought into decimal.js or big.js, you don't need Kahan; you're already exact.
  3. Real-time graphics/animation — A 3-4x slowdown for a pixel-perfect particle position is rarely worth it.
  4. Very small datasets — The error on [0.1, 0.2, 0.3] is 5.55e-17. Your users don't care.

10. One-Liners for Your Toolkit

For the copy-paste engineers:

// Kahan (functional, but slightly slower due to array destructuring)
const kahanSum = (a) => a.reduce(([s, c], x) => {
  const y = x - c;
  const t = s + y;
  return [t, (t - s) - y];
}, [0, 0])[0];

// Pairwise (iterative, stack-safe)
const pairwiseSum = (a) => {
  let q = [...a];
  while (q.length > 1) {
    const n = [];
    for (let i = 0; i < q.length; i += 2) n.push(q[i] + (q[i+1] || 0));
    q = n;
  }
  return q[0] || 0;
};
Enter fullscreen mode Exit fullscreen mode

Conclusion

Floating-point summation is one of those problems that seems trivial until it isn't. The good news: JavaScript gives you options.

  • Reach for pairwise summation when you need a quick accuracy boost without a performance cliff.
  • Reach for Kahan summation when precision is paramount and you're working with raw floats.
  • Reach for decimal.js when you're dealing with money, tax calculations, or anything where "close enough" isn't in your vocabulary.

And sometimes? Just use reduce. Not every array needs a PhD.


What's your go-to strategy for numerical accuracy in JavaScript? Drop your war stories in the comments below. 👇

Top comments (1)

Collapse
 
jonrandy profile image
Jon Randy 🎖️ • Edited

Never use floating point values for financial stuff. Integers - always. No library required.