Most production bugs around numbers are not algorithm bugs. They are representation bugs. You stored a value as a float. Somewhere downstream, it drifted. Now your invoice is off by a cent and accounting wants to know why.
If a value must be exact, I store it as an integer.
The core problem
Floating-point numbers are binary approximations of decimal values. Many decimal fractions cannot be represented exactly in binary.
0.1 + 0.2 === 0.3; // falseYou get 0.30000000000000004.
This is not a JavaScript bug. It is how IEEE 754 floating-point works across every language. And it hurts in real systems:
- Equality checks fail unexpectedly
- Repeated additions drift over time
- Rounding differs by language and runtime
- Serialization can change least-significant bits
- Cross-service comparisons become fragile
If your domain requires exactness, floats are the wrong default.
Where integers win
Integers are exact. Addition and subtraction on integers is deterministic. The trick is to pick a scale and stick with it:
- Money: store cents, not dollars
- Time: store milliseconds, not seconds as float
- Rates: store basis points, not percentages as float
- Counters: store whole units
Currency:
const priceCents = 1999; // $19.99
const taxCents = 150;
const totalCents = priceCents + taxCents; // 2149Percentages as basis points:
// 1% = 100 bps
const discountBps = 1250; // 12.50%
const amountCents = 10000;
const discounted = Math.floor((amountCents * (10000 - discountBps)) / 10000);No drift. No rounding surprises. The math is boring and that’s the point.
Where floats are fine
Floats are appropriate when approximation is expected — scientific computation, graphics, sensor data, statistical modeling, ML feature values. In these domains, “close enough” is acceptable and often intended.
Where floats are dangerous
Avoid floats for anything a human can dispute:
- Billing and invoices
- Ledger balances
- Payout calculations
- Deterministic workflows
- Idempotency keys derived from numeric values
- Reconciliation pipelines
If someone can look at a number and say “that’s wrong by a cent,” you used the wrong type.
Practical rules
Encode units in names
Bad: amount, price, timeout
Good: amountCents, priceCents, timeoutMs
The name carries the invariant. When a new developer reads amountCents, they know the scale without checking the docs.
Convert only at the edges
Keep internal logic integer-only. Convert at I/O boundaries:
- Display: cents to dollar string
- Input: decimal string to cents integer
Everything in between stays as integers. No intermediate float conversions, no accidental precision loss.
Define rounding policy once
If conversion is needed, centralize it. Round half up? Bankers rounding? Floor? Put the policy in one utility, test it with edge cases, and never duplicate the logic.
Keep transport integer-safe
- JSON: use integer fields for scaled values
- Database: use
BIGINTfor large totals - APIs: document unit and scale explicitly
The point
Using integers for core values is not about style. It’s about preserving truth over time. In production systems, exactness compounds just like errors do. A one-cent rounding error today is a thousand-dollar reconciliation problem next quarter.
Store it as an integer. Convert for display. Sleep at night.