Back to blog
JavaScriptNPM PackageAlgorithms

Doing Math With Millions of Digits in JavaScript

March 10, 20263 min readby Niloy Mahmud Apu

Try this in any JS console:

0.1 + 0.2;        // 0.30000000000000004
9999999999999999; // 10000000000000000

Neither is a bug. They're the honest consequences of JavaScript's Number being a 64-bit float. It has roughly 15–17 significant decimal digits — and once you exceed that, it silently rounds. For money, scientific work, or anything with a lot of digits, "silently rounds" is unacceptable. That problem is what led me to build ultra-calc.

Why Number breaks

A double stores values in binary scientific notation. Two things follow:

  1. Most decimals aren't representable. 0.1 has no exact binary form, the same way 1/3 has no exact decimal form. Small errors creep in and compound.
  2. There's a hard precision ceiling. Past ~2^53, integers stop being exactly representable — consecutive integers start sharing the same float.

BigInt helps — but only halfway

ES2020's BigInt gives you arbitrary-precision integers:

9999999999999999n + 1n; // 10000000000000000n  ✅ exact

That solves huge integers. It does not solve decimals — BigInt can't represent 0.1 at all — and it doesn't help with operations that produce fractional results. For "add these two numbers that each have a million digits, including a decimal point," you need something else.

The core idea: store digits, not floats

Arbitrary-precision arithmetic works by representing a number as its sequence of digits (in strings or integer arrays) plus a sign and a decimal position — then implementing arithmetic the way you learned it in school, digit by digit with carries. No floats anywhere in the hot path, so no rounding you didn't ask for.

import { add, multiply } from "ultra-calc";

add("0.1", "0.2");          // "0.3"  — exact, no surprises
multiply("99999999999999999999", "2"); // exact, well past Number's ceiling

Inputs are strings precisely so you never lose precision before the library even sees the value — add(0.1, 0.2) would already be wrong by the time the function ran.

What it costs

Precision isn't free. Schoolbook multiplication is O(n²) in the number of digits, so millions of digits means real CPU time. The honest engineering trade-offs:

  • Reach for it only when you need it. For everyday sizes, Number is faster and fine.
  • Keep values as strings end-to-end. Every round-trip through Number is a chance to corrupt the value.
  • Operation order matters once digit counts get large — the same result can cost very differently to compute.

When you actually need this

  • Money, where a fraction of a cent of drift across millions of rows is a real bug.
  • Scientific / cryptographic work that needs exact large integers or high-precision decimals.
  • Education and puzzles — factorials, Fibonacci, π to a stupid number of places.

I also wrapped it in a small UI, Calculate Big, so you can feel the difference without installing anything.

The takeaway

Floating point isn't broken — it's a deliberate trade of precision for speed, and for most code that's the right trade. But the moment your numbers get big or your decimals need to be exact, you have to stop trusting Number. Know where its edges are, reach for BigInt for integers, and for everything in between, store the digits and do the arithmetic yourself.