HomeJournalThis post

Math.sumPrecise for Reliable Totals

Compare Math.sumPrecise with reduce across cancellation, order, unbounded values, negative zero, and invalid values, then choose the right numeric domain.

JP
JP Casabianca
AI Engineer and Product Designer · full-stack delivery · Bogotá

Math.sumPrecise answers when a JavaScript product total changes because addition order loses low-order information. This tutorial runs a deterministic corpus against naive reduce and a compact reference oracle, then marks the boundary where decimal-domain arithmetic is still necessary.

Math.sumPrecise changes the accumulation algorithm

Ordinary Array.prototype.reduce with addition rounds after every step. When a partial sum is much larger than the next term, that term can disappear; a later opposite large term may then leave the wrong remainder. Math.sumPrecise consumes an iterable of Numbers and applies the specification's more accurate summation method, reducing sensitivity to order and catastrophic cancellation.

The ECMAScript 2026 section defines current behavior. Read it for iteration, abrupt completion, infinities, and signed zero rather than treating the method as a renamed reduce. An accurate JavaScript sum is still a binary floating-point result with the representable limits of Number.

The teaching fixture corpus contains local arrays for cancellation, permutations, empty input, negative zero, unbounded values, and invalid values. A compact expansion-based oracle supplies expected finite totals where the native method is unavailable. The fixture labels runtime support explicitly and does not claim financial correctness or production measurement.

Store the original term sequence with a failed numerical fixture. Sorting solely to obtain a stable total can erase temporal or provenance meaning needed for debugging.

Cancellation balanceA tiny term sits between a large positive and large negative term, comparing naive and precise accumulation.large + tinylarge negativeexact total: tiny
  • Naive left-to-right addition may round the tiny addend away.
  • A precise summation algorithm preserves enough error information to recover the mathematical sum when representable.
  • Binary floating point still does not become decimal money arithmetic.
Cancellation balance reading key
SignalInterpretation
Cancellation balanceA tiny term sits between a large positive and large negative term, comparing naive and precise accumulation.
Figure 1: Cancellation exposes why accumulation method matters even when every input is a Number.

See how a tiny term disappears

Consider a large positive term, a small term, and the matching large negative term. The mathematical total is the small term. A left-to-right reducer may add the small value to a partial sum whose unit in the last place is larger, round it away, then cancel the large terms to zero. Reordering the small value after cancellation can yield a different result.

This is catastrophic cancellation in the accumulation path. It does not require faulty hardware or an invalid input; it follows from finite representation and operation order. Math.sumPrecise retains enough intermediate error information to deliver the correctly rounded sum required by its algorithm for the iterable, avoiding common loss patterns.

Use the cancellation balance as a debugging prompt. If a metric combines large offsets with small corrections, an order-dependent result is plausible. The AI agent cost attribution workflow is one example where many heterogeneous numeric components deserve a declared aggregation method, though actual billing may require decimal units.

Inspect magnitude distribution as well as array length. A short sequence spanning many orders of magnitude can be more fragile than a long, well-scaled collection.

Run order permutations as a diagnostic

Generate several deterministic permutations of the same multiset and compare totals. A naive reducer may produce multiple values because its rounding path changes. A stable result across a few orders is not proof of accuracy, but disagreement is strong evidence that accumulation deserves attention. Include sorted ascending magnitude, descending magnitude, original order, and a seeded shuffle.

Floating-point summation can be improved through pairwise, compensated, expansion, or specification-defined algorithms with different performance and correctness properties. Do not label every helper sumPrecise unless it implements the required semantics. Math.sumPrecise gives platform code a shared contract, while the fixture's oracle is intentionally small and limited to its finite teaching cases.

The order ribbon visualizes result classes rather than invented performance values. Its paths correspond to fixture orders, and the local artifact prints the naive and oracle totals. Use case-study metrics without theater when presenting real measurements so the method, dataset, and uncertainty stay attached.

Seeded permutations belong in regression tests because a later refactor may change iteration order without changing the declared input set. The receipt makes that shift visible.

The numerical corpus prints naive, oracle, and native-labeled receipts across cancellation, edge semantics, permutations, and iterator cleanup.

Runnable artifact — sum-precise-corpus.mjs

import assert from "node:assert/strict";
const twoSum = (a, b) => { const sum = a + b, tail = sum - a; return [sum, (a - (sum - tail)) + (b - tail)]; };
const oracle = (iterable) => {
  let parts = [], count = 0, onlyNegativeZero = true, positiveUnbounded = false, negativeUnbounded = false;
  for (const value of iterable) {
    count += 1;
    if (typeof value !== "number") throw new TypeError("fixture accepts numbers only");
    if (value !== value) return 0 / 0;
    if (value === 1 / 0) { positiveUnbounded = true; continue; }
    if (value === -1 / 0) { negativeUnbounded = true; continue; }
    if (!Object.is(value, -0)) onlyNegativeZero = false;
    let nextValue = value, nextParts = [];
    for (const part of parts) { const [sum, error] = twoSum(nextValue, part); if (error) nextParts.push(error); nextValue = sum; }
    nextParts.push(nextValue); parts = nextParts;
  }
  if (positiveUnbounded && negativeUnbounded) return 0 / 0;
  if (positiveUnbounded) return 1 / 0;
  if (negativeUnbounded) return -1 / 0;
  if (count === 0 || onlyNegativeZero) return -0;
  return parts.reduce((sum, value) => sum + value, 0);
};
const naive = (values) => values.reduce((sum, value) => sum + value, 0);
const display = (value) => value !== value ? "not-a-number" : Object.is(value, -0) ? "negative-zero" : value === 1 / 0 ? "positive-unbounded" : value === -1 / 0 ? "negative-unbounded" : String(value);
assert.ok(Object.is(oracle([]), -0));
assert.ok(Object.is(oracle([-0, -0]), -0));
assert.ok(Object.is(oracle([-0, 0]), 0));
assert.equal(oracle([1 / 0]), 1 / 0);
assert.equal(oracle([-1 / 0]), -1 / 0);
assert.ok(oracle([1 / 0, -1 / 0]) !== oracle([1 / 0, -1 / 0]));
assert.throws(() => oracle([1, "2"]), TypeError);
let closed = false;
const cleanupIterable = { [Symbol.iterator]() { let index = 0; return { next() { return index++ === 0 ? { value: 1, done: false } : { value: "bad", done: false }; }, return() { closed = true; return { done: true }; } }; } };
assert.throws(() => oracle(cleanupIterable), TypeError);
assert.equal(closed, true);
const permutations = [[1e16, 1, -1e16], [1e16, -1e16, 1], [1, 1e16, -1e16]];
for (const values of permutations) assert.equal(oracle(values), 1);
const nativeAvailable = typeof Math.sumPrecise === "function";
if (nativeAvailable) assert.equal(Math.sumPrecise(permutations[0]), 1);
const printed = permutations.map((values, index) => ({ case: index + 1, naive: display(naive(values)), oracle: display(oracle(values)), native: nativeAvailable ? display(Math.sumPrecise(values)) : "oracle-fallback" }));
console.table(printed);
console.log(JSON.stringify({ empty: display(oracle([])), signedZero: display(oracle([-0, -0])), mixedZero: display(oracle([-0, 0])), iteratorClosed: closed, nativeLabel: nativeAvailable ? "native" : "oracle-fallback" }));
console.log("PASS: oracle preserves the cancellation remainder");

Run node sum-precise-corpus.mjs. Expected receipt: PASS: oracle preserves the cancellation remainder.

Order permutation result ribbonFour orderings of the same terms weave into naive result bands while the precise result stays aligned.four input ordersprecise reference alignment
  1. Each curved ribbon is one naive reduction order.
  2. Different endpoints reveal order-sensitive rounding.
  3. The dashed baseline is the fixture oracle used for comparison.
Order permutation result ribbon reading key
SignalInterpretation
Order permutation result ribbonFour orderings of the same terms weave into naive result bands while the precise result stays aligned.
Figure 2: Permuting identical terms is a compact diagnostic for unstable accumulation.

Respect iterable and type semantics

The input is an iterable, not specifically an array. That allows sets, generators, and custom iterators, but it also means iteration can throw or have cleanup behavior. Test a generator that yields valid Numbers, a generator that throws, and an iterator with a return method. Do not eagerly spread an untrusted or unbounded iterable just to call the function.

Values must follow the specification's Number requirements; this is not a coercing spreadsheet SUM. Strings, BigInts, or objects should not be silently converted by a wrapper unless the product defines that preprocessing separately. A strict type boundary keeps a stray formatted value from changing the aggregation domain. Math.sumPrecise should sit after schema validation, not replace it.

The TypeScript runtime boundaries article explains why a static number type cannot validate external JSON. Parse units and numeric domain at ingestion, reject non-finite values when the product forbids them, then select the summation contract.

Close custom iterators in failure tests and preserve the primary exception. Numerical accuracy is not permission to leak a stream, file cursor, or generator resource.

Test unbounded values and signed zero

Numerical edge cases are observable API behavior. Opposed positive and negative unbounded values cannot yield a finite sum. An indeterminate numeric result propagates according to the algorithm. Empty input and collections of negative zeros have specified signed-zero results that can be detected with Object.is even though ordinary equality considers the zero values equal.

Read the standard rather than inferring behavior from one current engine. ECMAScript 2026 Math includes detailed steps for these cases, and the proposal specification preserves useful design context. Math.sumPrecise tests should assert Object.is for zero sign and Number.isNaN where appropriate.

Product code may reasonably normalize negative zero for display, reject unbounded values at validation, or map a calculation error to a named state. Make that an explicit layer after summation. The semantic grid keeps algorithm output separate from the UI policy that decides what users see.

Display formatting must happen after the numeric-domain decision. Rounding each input for presentation and then summing those strings tests a different product rule.

Feature-detect without changing meaning

Check typeof Math.sumPrecise === "function" at the supported-runtime boundary. If present, run the native implementation. If absent, choose a reviewed polyfill or a deliberately weaker fallback and label the capability. Do not silently use reduce while presenting the result as though it has precise-summation guarantees.

A polyfill must match iterable, exception, unbounded-value, indeterminate-result, and signed-zero behavior, not merely the cancellation example. Import cost and performance also belong in qualification. Math.sumPrecise may be unnecessary for short well-scaled arrays, but the choice should come from a bounded workload and accuracy contract rather than intuition.

The artifact feature-detects the native method and always checks its local oracle corpus. This provides deterministic teaching evidence in runtimes both with and without native support. Browser support and performance still require a separately committed matrix on the actual deployment targets.

Pin the fallback package and corpus together. An implementation update should rerun edge semantics before the dependency becomes the default on unsupported engines.

Summation edge-case gridA semantic grid classifies empty input, negative zero, unbounded values, cancellation, and non-number values.empty iterablenegative zerounbounded valuescancellationtype error
CaseQuestion
Empty and zerowhat sign is returned?
Opposed unbounded valueswhen is an indeterminate result required?
Non-numberdoes iteration coerce or throw?
Summation edge-case grid reading key
SignalInterpretation
Summation edge-case gridA semantic grid classifies empty input, negative zero, unbounded values, cancellation, and non-number values.
Figure 3: Accuracy and API semantics belong in the same test corpus.

Keep decimal domains decimal

More accurate binary summation does not make 0.1 exactly representable, enforce currency rounding rules, preserve arbitrary decimal scale, or attach units. Money, tax, accounting, and regulated quantities often need integer minor units or a decimal library with explicit rounding. Convert at a validated boundary and never mix domains invisibly.

Math.sumPrecise is valuable for scientific values, geometry, telemetry, probabilities, and other Number-based aggregates where cancellation or long sequences matter. The LLM eval confidence intervals context likewise needs a documented statistical and numeric method. Accuracy of addition cannot compensate for biased sampling or invalid observations.

Write the domain in the function name and type: sumFloatSamples, sumMinorUnits, or sumDecimalAmounts communicates more than total. The method answers how to add; the product contract answers what the numbers mean and which rounding is permitted.

For minor-unit integers, also check safe-integer bounds and overflow policy. Switching away from binary fractions does not remove every numerical limit in JavaScript.

Adopt with a numerical receipt

Identify one aggregation boundary, freeze representative and adversarial inputs, run naive, current, and reference implementations, and record differences. Add order permutations and edge cases. If results affect visible values, document display rounding and migration behavior so a corrected total does not look like unexplained data drift.

Benchmark only after correctness tests pass, using the actual array lengths, iterable shapes, engines, and warm-up policy. Do not generalize a microbenchmark into universal speed claims. Math.sumPrecise adoption should be gated by acceptable correctness, supported runtime coverage, and measured cost for the product workload.

The practical finish is a small receipt: input-domain declaration, corpus hash, native-support flag, algorithm result, display policy, and chosen fallback. Run that corpus against one real boundary before switching it. The artifact below supplies the cancellation core, not a substitute for your own authorized inputs.

When corrected aggregation changes historical output, version the calculation and explain the transition. Silent recomputation can look like data corruption even when mathematics improved. Preserve both receipts during review so the changed accumulation path remains explainable.