Loading post
Jul 23, 2026

Open your browser's dev tools, a Python REPL, whatever you have handy, and type this:
0.1 + 0.2
// → 0.30000000000000004
It doesn't equal 0.3. It's not a bug. You get the same result in nearly every language.
Payments is my day job — at PayPal, and now at a startup, my work is moving money without losing a cent of it. The very first rule you get drilled into you in that world is: never store money in a float. This post covers why 0.1 + 0.2 isn't 0.3, why that's fatal for payments, how you're actually supposed to hold money — and concretely how to do it in TypeScript, Java, Python, C, and Go. It's long, but the goal is that this one post teaches you "how to hold money." Even if you rarely write code, the first half should leave you thinking "huh, so that's how computers see numbers."
It's not just 0.1 + 0.2. Make the computer do anything money-shaped and little decimal gremlins crawl out everywhere. Here's what JavaScript actually returns:
0.1 + 0.2 // → 0.30000000000000004
0.7 * 100 // → 70 (this one happens to be fine)
1.1 * 3 // → 3.3000000000000003
Math.round(1.005 * 100) // → 100 ← you expected 101!
(2.675).toFixed(2) // → "2.67" ← should be 2.68! (even the built-in toFixed is wrong)
(0.1 + 0.2) === 0.3 // → false
The fourth and fifth are quietly terrifying. You wanted to "round 1.005 to the nearest cent," so you * 100 and rounded — and got 100, when it should have been 101. And the fifth: the language's own toFixed(2) turns 2.675 into 2.67, not 2.68. This isn't "you wrote it wrong" — it's the nature of floating point itself.
One transaction is noise; but a payments system does this millions of times, and by month-end you're staring at a reconciliation report that's "off by a cent," which is its own special hell.
The reason is simple: computers store numbers in binary (0s and 1s).
Even in our everyday base-10, some numbers don't divide cleanly. 1 ÷ 3 = 0.3333.... You can't write 1/3 neatly in decimal; you can only keep an approximation truncated somewhere.
The same thing happens in binary. To a human, 0.1 looks clean — but write it in binary and you get 0.0001100110011..., repeating forever. In binary, 0.1 is one of those numbers that doesn't divide cleanly. So the computer doesn't store 0.1 itself; it stores the closest approximation it can.
Base-10 world: 1/3 = 0.3333... ← can't write it cleanly
Binary world: 0.1 = 0.00011001... ← can't write it cleanly (!)
Add two of those approximations (0.1 + 0.2) and their little errors add up too, landing you at 0.30000000000000004 — right next door to the real 0.3. This scheme has a formal name: IEEE 754, the standard for floating-point numbers. That's what a float or double actually is.
The key point: floats are "fast and roughly accurate," but not exact. In physics or machine learning, roughly accurate is plenty, and floats shine. But money is different. Money can't be "roughly." A yen is a yen, not 0.9999999 of a yen.
Three reasons.
999.9999999 is already a bug.0.0000001 drift per transaction adds up over millions of transactions a day into a visibly wrong amount by month-end.So in payments, exactness is a functional requirement. How do you hold money? Three rules.
Three rules for holding money in payments
① Default to integer minor units (cents for dollars, yen for yen)
② Use a Decimal type (an exact base-10 type) only when you truly need fractions
③ Never use float / double
Let's go through them.
The simplest, industry-standard answer: don't use decimals — hold the currency's smallest unit as an integer.
For dollars, hold cents. Store $10.50 not as 10.5 (a decimal) but as 1050 (an integer number of cents). Integer addition and subtraction carry zero floating-point error, so 1050 + 2075 = 3125 is exact forever.
// ❌ bad: hold a decimal
let priceDollars = 10.50;
let total = priceDollars * 3; // can become 31.499999999999996
// ✅ good: hold the minor unit (cents) as an integer
let priceCents = 1050; // $10.50 held as 1050 cents
let totalCents = priceCents * 3; // 3150. Exact, forever.
// Convert back to decimal only at the moment you display
const display = `$${(totalCents / 100).toFixed(2)}`; // "$31.50"
This is exactly why a payments API like Stripe passes amounts as amount: 1050 (cents). The rule: store and compute in integer minor units; convert to decimal only for the split second you paint the screen.
Here's the trap most people fall into: "minor unit means I just divide everything by 100, yeah?" No. The number of decimal places varies by currency.
Currency Minor unit Example
──────────────────────────────────────────────────
Yen (JPY) 1 yen (no decimals) ¥1000 → 1000
Dollar (USD) 1 cent = 1/100 $10.50 → 1050
Dinar (BHD) 1/1000 1.234 → 1234
Some crypto 1/100000000 down to 0.00000001
──────────────────────────────────────────────────
The Japanese yen has no practical sub-unit, so 1000 in yen isn't "1000 sen," it's 1000 yen itself. The Bahraini dinar goes to three decimal places. "amount × 100" is too much for yen and too little for the dinar.
Keep the number of decimal places per currency (defined by ISO 4217).
// Decimal places per currency (minor unit exponent)
const CURRENCY_DECIMALS: Record<string, number> = {
JPY: 0, // yen: the minor unit is the yen itself
USD: 2, // dollar: 1/100
EUR: 2,
BHD: 3, // dinar: 1/1000
};
Turn a user's string "10.50" into the integer minor unit 1050. There's a "simple" way and a "robust" way, and which one to use is the most practical point in this whole post.
function toMinorUnitsSimple(amount: string, currency: string): number {
const decimals = CURRENCY_DECIMALS[currency] ?? 2;
return Math.round(parseFloat(amount) * 10 ** decimals);
}
toMinorUnitsSimple('10.50', 'USD'); // 1050
toMinorUnitsSimple('1000', 'JPY'); // 1000
Right after parseFloat(amount) * 10 ** decimals, gremlins like 4.35 * 100 = 434.99999... can be sitting there. But if the input only ever has that many decimals to begin with, the error is far below 0.5 and the final Math.round recovers the correct integer. For a normal app, honestly, this is fine.
But the simple version is only betting that the error stayed under 0.5, so it breaks in these cases. This is the concrete "why you can't lean on the simple version."
① Rounding an input with more precision than the target goes the wrong way.
(1.005).toPrecision(20); // "1.0049999999999998..." ← stored just *below* 1.005
Math.round(1.005 * 100); // → 100 (you expected 101!)
(2.675).toFixed(2); // → "2.67" (should be 2.68; even the built-in toFixed is wrong)
Both 1.005 and 2.675 are stored slightly below their decimal value in binary, so rounding to nearest rounds them down. This is the textbook case where "just multiply and round" fails — and it happens the moment an interest or discount calculation produces more precision than the target.
② Error grows as you stack operations, until round can't save it.
let s = 0;
for (let i = 0; i < 10; i++) s += 0.1;
s; // → 0.9999999999999999
Here Math.round(s * 100) still recovers 100. But in a longer calculation with multiplications and divisions, the moment the error crosses 0.5, round can't save it anymore. "Just round at the end" is only betting that the error stayed under 0.5.
③ For large magnitudes, the float loses integer precision entirely (the JS trap, below).
So the simple version is fine for "one clean conversion of a small input," but wrong, silently, everywhere else. That's why payments code uses the robust version.
Payments code, where exactness is the point, never creates a float via parseFloat — it converts straight from the string to an integer, so it never depends on the "error under 0.5" assumption.
function toMinorUnits(amount: string, currency: string): number {
const decimals = CURRENCY_DECIMALS[currency] ?? 2;
const [intPart, fracPart = ''] = amount.split('.'); // "10.5" → ["10","5"]
// Force the fraction to be exactly `decimals` digits.
// One expression handles both "too short → pad" and "too long → truncate":
// "5" → "5"+"00"="500" → first 2 chars "50" (the zeros padded it)
// "50" → "50"+"00"="5000" → first 2 chars "50" (the added zeros just get dropped)
// "999" → "999"+"00"=… → first 2 chars "99" (truncated)
const frac = (fracPart + '0'.repeat(decimals)).slice(0, decimals);
return Number(intPart + frac); // "10" + "50" = "1050" → 1050
}
The '0'.repeat(decimals) is insurance for short inputs. For an already-long-enough input like "10.50", the appended zeros are simply sliced away and do nothing (which is why they look pointless there). For "10.5" or "10", those zeros are what pad it out. That's how both "10.5" and "10.50" land on the same 1050.
That said, you rarely hand-roll this — the real industry standard is "use a money/decimal library," which internally does exactly this (handle the value as a string / base-10 number, never a binary float). Per-language libraries are collected below.
As ① and ② showed: it works for one clean conversion, but not in general. Round is useful, but it's not an excuse to use a float. The right policy isn't "rescue it with round afterward" — it's to hold integer minor units so no floating-point error is ever created, and then you never have to pray round saves you.
Integer minor units are enough for "moving fixed amounts around." But interest, per-unit price times quantity, currency conversion — those need fractions.
For those, don't use float; use a Decimal type that represents base-10 exactly, so 0.1 + 0.2 actually equals 0.3. The cost is slower and heavier than float — the "fast but inexact float" vs. "slow but exact Decimal" trade-off, and for money you always take exact. Here's Python actually running:
from decimal import Decimal
Decimal("0.1") + Decimal("0.2") # → Decimal('0.3') ← actually 0.3!
Decimal(0.1) # → Decimal('0.1000000000000000055511...') ← built from a float = trap
That last line matters: building a Decimal from a float drags the float's error in with it. Always build from a string. This is a trap in every language.
Even holding integers exactly, division is sometimes unavoidable — splitting a bill. Divide ¥1000 three ways and you get 333.33.... Someone eats an extra yen or gains one.
Naively summing Math.round(1000 / 3) three times gives 333 × 3 = 999 — a yen vanishes. Round up and 334 × 3 = 1002 — two yen appear from nowhere. The total no longer matches — disqualifying for a ledger.
The correct approach distributes the remainder deterministically, like the largest-remainder method.
// Split ¥1000 across 3 people so the total is always exactly 1000
function splitEvenly(total: number, n: number): number[] {
const base = Math.floor(total / n); // 333
const remainder = total - base * n; // 1000 - 999 = 1 (the leftover yen)
return Array.from({ length: n }, (_, i) => base + (i < remainder ? 1 : 0));
// → [334, 333, 333] total is exactly 1000
}
In Settli, the bill-splitting app I built, "who eats the remainder" is an explicit policy. Among friends, "nobody eats anything under half a yen (silently drop it)" is fine; in a production ledger you track the dropped remainder in a rounding account and allocate it deterministically. Same arithmetic, different social contract.
Now the implementation core. Across TypeScript / Java / Python / C / Go, here's "the wrong way → integer minor units → the standard decimal library when you need fractions." The conclusion is the same everywhere: default to integer minor units, use a Decimal (not a float) for fractions, and never go through a float.
JS has no integer type; every number is a 64-bit float (double). That's the biggest gotcha.
// ❌ bad: every number is a double
const total = 0.1 + 0.2; // 0.30000000000000004
// ✅ integer minor units. number holds integers exactly up to 2^53 (~9 quadrillion)
let priceCents = 1050;
let totalCents = priceCents * 3; // 3150
// If you can exceed 2^53, use BigInt (trailing n)
let huge = 9_007_199_254_740_993n; // bigint
// ✅ for fractions use decimal.js; for a money type use dinero.js
import Decimal from 'decimal.js';
new Decimal('0.1').plus('0.2').toString(); // "0.3"
new Decimal('1.005').times(100).toString(); // "100.5" (no drift)
Java has both double and the exact BigDecimal. The trap is building BigDecimal from a double.
// ❌ bad: double is inexact
double total = 0.1 + 0.2; // 0.30000000000000004
// ❌ worse: BigDecimal from a double drags the error in
new BigDecimal(0.1); // 0.1000000000000000055511151231257827021181...
// ✅ integer minor units as long
long priceCents = 1050L;
long totalCents = priceCents * 3; // 3150
// ✅ build BigDecimal FROM A STRING
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
a.add(b); // 0.3
new BigDecimal("1.005")
.setScale(2, RoundingMode.HALF_UP); // 1.01 (rounds correctly)
// For a money type: JSR-354 (javax.money / Moneta) or Joda-Money
Python's int is arbitrary-precision (no overflow), and the standard library ships decimal.Decimal.
# ❌ bad: float is inexact; Decimal from a float is a trap
0.1 + 0.2 # 0.30000000000000004
Decimal(0.1) # Decimal('0.1000000000000000055511151231257827...')
# ✅ integer minor units (int is arbitrary-precision, no overflow)
price_cents = 1050
total_cents = price_cents * 3 # 3150
# ✅ build Decimal FROM A STRING
from decimal import Decimal, ROUND_HALF_UP
Decimal("0.1") + Decimal("0.2") # Decimal('0.3')
Decimal("1.005").quantize(Decimal("0.01"), ROUND_HALF_UP) # Decimal('1.01')
# For a money type: py-moneyed
The C/C++ standard long had no decimal type, so fixed-point (a 64-bit integer of cents) is the classic move.
#include <stdint.h>
#include <stdio.h>
// ❌ bad: double is inexact
double total = 0.1 + 0.2; // 0.30000000000000004...
// ✅ integer minor units in a 64-bit integer
long long price_cents = 1050;
long long total_cents = price_cents * 3; // 3150
// Convert back to decimal only for display (integer quotient + remainder)
printf("$%lld.%02lld\n", total_cents / 100, total_cents % 100); // $31.50
// note: for negative amounts, mind the sign (remainder can be negative)
If you genuinely need fractions, options include C23's _Decimal64 (decimal floating point, IEEE 754-2008), GCC's decimal-float extension, or Boost.Multiprecision's cpp_dec_float in C++. But most payment logic runs fine on integer cents.
Go also has no built-in decimal — use integer minor units (int64) or the de-facto standard shopspring/decimal.
// ❌ bad: float64 is inexact
total := 0.1 + 0.2 // 0.30000000000000004
// ✅ integer minor units as int64
priceCents := int64(1050)
totalCents := priceCents * 3 // 3150
// ✅ for fractions use shopspring/decimal (Go's de-facto standard)
import "github.com/shopspring/decimal"
a, _ := decimal.NewFromString("0.1")
b, _ := decimal.NewFromString("0.2")
a.Add(b) // 0.3
// For a money type: rhymond/go-money
The shared pattern across all five:
Language Integer minor unit Standard decimal "from a float" trap
──────────────────────────────────────────────────────────────────────────
TS/JS number / bigint decimal.js / dinero new Decimal(0.1)
Java long BigDecimal new BigDecimal(0.1) ← famous
Python int (bignum) decimal.Decimal Decimal(0.1)
C/C++ int64_t _Decimal64 / Boost (just don't use double)
Go int64 shopspring/decimal decimal.NewFromFloat(0.1)
──────────────────────────────────────────────────────────────────────────
Shared rule: default to integer minor units, build Decimals FROM A STRING, never go through a float
Finally, three you'll hit across languages.
① Parse straight through to an integer. Don't store or compute with the float from parseFloat. Convert to an integer minor unit immediately; after that the error is gone.
② JavaScript numbers are all doubles. As noted, integers above ~9 quadrillion (2^53) can't be held precisely as integers. Yen amounts have lots of digits, so watch out:
Number.MAX_SAFE_INTEGER; // 9007199254740991
Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2; // → true (!)
③ Mind your database's type. My stack is Firebase/Firestore, and Firestore numbers are all doubles internally (no dedicated decimal type). So the moment you "store 0.1 in Firestore," you're in floating-point land. Store money as an integer minor unit or a string. It's worth checking how your DB holds numbers — some (PostgreSQL NUMERIC, MySQL DECIMAL) do have exact decimal types.
0.1 + 0.2 isn't 0.3 because 0.1 doesn't divide cleanly in binary — the same phenomenon as 1/3 in decimal. A spec, not a bug.1.005, 2.675, large magnitudes, and stacked operations. Payments use the robust version that never creates a float."Don't store money in a float" is the first rule a payments engineer learns and the last one they forget. And behind a tiny party trick like 0.1 + 0.2 hides a genuinely deep story about how computers see numbers at all.