Doing Math With Millions of Digits in JavaScript
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:
- Most decimals aren't representable.
0.1has no exact binary form, the same way1/3has no exact decimal form. Small errors creep in and compound. - 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,
Numberis faster and fine. - Keep values as strings end-to-end. Every round-trip through
Numberis 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.
Keep reading
Why I Route Contact Messages to Telegram (With Email as a Fallback)
Email contact forms are easy to ignore. Routing portfolio messages to a Telegram bot gets them on my phone instantly — and a fallback means I never lose one.
Building a Multi-Platform Live Chat Widget in Next.js
How I added a floating chat widget to my portfolio that lets visitors reach me on Telegram, WhatsApp, or Messenger — with a message-drop fallback that emails me directly.