Skip to content
Back to Blog
Tutorials

Floating Point Precision: Why 0.1 + 0.2 ≠ 0.3 (2026 Guide)

Why floating point precision breaks 0.1 + 0.2, how IEEE 754 rounds, and how to compare floats safely in JS, Python and SQL — with a free online converter.

12 min read

Floating Point Precision: Why 0.1 + 0.2 ≠ 0.3 (2026 Guide)

0.1 + 0.2 returns 0.30000000000000004 because neither 0.1 nor 0.2 exists inside a binary floating-point number. A double can only hold values of the form m × 2ⁿ, and one tenth is an infinite repeating fraction in base 2, so the hardware keeps the nearest representable neighbour instead. The double behind 0.1 is exactly 0.1000000000000000055511151231257827021181583404541015625. The one behind 0.2 is 0.200000000000000011102230246251565404236316680908203125. Add those two stored values, and the exact sum lands between two representable doubles. IEEE 754 rounds to the nearer one, which sits a hair above 0.3.

That is the whole answer. Floating point precision is finite, and most decimal fractions miss the binary grid, so rounding happens on the way in and again after every operation.

This is not a JavaScript quirk or a CPU defect. The same result appears in Python, Java, C, Go, Rust, Swift, and SQL’s FLOAT columns, because they all run on IEEE 754. Paste any value into the free IEEE 754 converter to see the exact bits and the exact stored value, digit for digit.

The error itself is tiny. The damage comes later, when a comparison silently returns false or a FLOAT column stops reconciling with the ledger.

Floating Point Precision in 60 Seconds

Three facts explain almost every surprise:

  1. Binary floating point can represent only numbers of the form m × 2ⁿ, sums of powers of two.
  2. Decimal fractions like 0.1, 0.2, and 0.3 are not of that form, so they get rounded on the way in.
  3. Every arithmetic operation rounds its result to the nearest representable value again.
DecimalExactly representable?Why
0.5Yes2⁻¹
0.25Yes2⁻²
0.75Yes2⁻¹ + 2⁻²
2.5Yes2 + 2⁻¹
100YesAny integer below 2⁵³
0.1No1/10, denominator has a factor of 5
0.2No1/5, same problem
0.3No3/10, same problem

Rule of thumb: reduce the fraction, and if the denominator is a pure power of two, the value is exact. Any surviving factor of 5 means the value is stored approximately.

Why 0.1 Cannot Be Stored Exactly

The bits after a binary point carry the weights 1/2, 1/4, 1/8, 1/16, 1/32, and so on. Try to assemble 0.1 from them and you never land on it. You get 1/16 = 0.0625, plus 1/32 = 0.09375, plus 1/256 = 0.09765625, closer each time, never exact. In binary, one tenth is 0.0001100110011… with 0011 repeating forever.

Decimal has the same problem with different fractions. Writing 1/3 in base 10 gives 0.333… and no finite string of digits ever nails it. Nobody calls that a bug in decimal. Base 2 simply draws the line in a different place, and 1/10 happens to fall on the wrong side of it. The Python documentation’s Floating Point Arithmetic: Issues and Limitations walks through the same expansion if you want the long form.

Binary is just base 2, and the same positional arithmetic drives hex and octal. The number base converter shows how a value moves between bases, and our number base conversion guide covers the integer side in depth. Fractions are where base 2 starts to hurt.

What a double actually stores

A 64-bit double splits into three fields: 1 sign bit, 11 exponent bits, and 52 mantissa bits. The mantissa carries an implicit leading 1 that is never stored, so you get 53 bits of significand, roughly 15.95 decimal digits of floating point precision. The exponent carries a bias of 1023.

Ask any language for more digits than it normally prints and the approximation appears:

>>> 0.1
0.1
>>> f"{0.1:.20f}"
'0.10000000000000000555'
>>> from decimal import Decimal
>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
(0.1).toFixed(20);      // '0.10000000000000000555'
(0.1).toPrecision(20);  // '0.10000000000000000555'

Decimal(0.1) is the honest view: it converts the double you already have, without re-rounding, and prints all 55 digits. The accuracy panel in the IEEE 754 converter prints the same expansion for any number you type, next to the signed rounding error against what you entered.

The Exact Arithmetic Behind 0.30000000000000004

The next question is why the result is that specific number rather than some other near miss.

Add the two stored values exactly, with no rounding, and you get:

0.1 →  0.1000000000000000055511151231257827021181583404541015625
0.2 →  0.200000000000000011102230246251565404236316680908203125
sum →  0.3000000000000000166533453693773481063544750213623046875

That sum is not itself a representable double. It sits between two neighbours on the binary grid:

below:  0.299999999999999988897769753748434595763683319091796875   (prints as 0.3)
above:  0.3000000000000000444089209850062616169452667236328125     (prints as 0.30000000000000004)

The gaps are where it gets specific. The exact sum sits 2⁻⁵⁵2.776e-17 above the lower neighbour and the same 2⁻⁵⁵ below the upper one, so it is not closer to either. It lands exactly halfway.

Round-to-nearest has no distance to work with here, so IEEE 754 applies its tie-breaker: round-half-to-even, which picks the candidate whose last mantissa bit is 0. The lower neighbour’s significand is 5404319552844595, which is odd. The upper one’s is 5404319552844596, which is even. The even value wins, and the result carries the bit pattern 0x3FD3333333333334, one ULP above the double that the literal 0.3 maps to, printed as 0.30000000000000004.

Why prefer even? Always rounding halves upward would bias long sums in one direction. Alternating toward the even neighbour keeps that drift near zero over many operations. It is the same banker’s rounding accountants use, now standardised in silicon, and it is why this particular floating point rounding error ends in ...04 rather than anywhere else.

Contrast that with a sum that needs no rounding at all:

0.5 + 0.25 === 0.75;   // true
0.1 + 0.2 === 0.3;     // false

0.5, 0.25, and 0.75 are 2⁻¹, 2⁻², and 2⁻¹ + 2⁻². All three are exact, their sum is exact, and equality behaves the way school arithmetic suggests. The converter’s neighbours panel shows the previous and next representable values around whatever you enter, along with the ULP gap, so you can check the grid around 0.3 yourself.

Why Your Language Sometimes Hides the Error

0.1 prints as 0.1, but 0.1 + 0.2 prints seventeen digits. Nothing about the storage format changed between those two lines, which is what makes the behaviour feel haunted.

Modern runtimes use shortest round-trip formatting: they emit the shortest decimal string that parses back to the identical double. "0.1" already round-trips uniquely to the value stored for 0.1, so that is what you see. The sum 0.1 + 0.2 is a different double from the one "0.3" maps to, so the formatter must keep adding digits until the string is unambiguous, and that takes all seventeen. David Gay’s 1990 correct-rounding work started this lineage; Grisu and then Ryu made it fast enough for every standard library. Your language is not lying. It shows the shortest label that uniquely identifies the value.

Language-by-language behaviour

LanguageDefault float type0.1 + 0.2 printsExact-decimal option
JavaScriptnumber (double only)0.30000000000000004None built in; integer cents or a library
Pythonfloat (double)0.30000000000000004decimal.Decimal, fractions.Fraction
Javadouble0.30000000000000004BigDecimal (build it from a string)
C#double0.30000000000000004decimal (128-bit, base 10)
Gofloat640.30000000000000004 (via variables)math/big.Rat
Rustf640.30000000000000004rust_decimal crate
SQLFLOAT / DOUBLE PRECISION0.30000000000000004NUMERIC / DECIMAL

Two of those rows hide a trap.

Go folds constants at arbitrary precision. The compiler evaluates a literal expression exactly and converts only afterwards, so the constant 0.1 + 0.2 becomes exactly 0.3 before it ever becomes a float64:

package main

import "fmt"

func main() {
	fmt.Println(0.1 + 0.2) // 0.3   — constant folded exactly, then converted
	a, b := 0.1, 0.2
	fmt.Println(a + b)      // 0.30000000000000004
	fmt.Println(a+b == 0.3) // false
}

Java’s BigDecimal inherits the error if you feed it a double. The constructor faithfully converts whatever bits it receives:

System.out.println(new BigDecimal(0.1));
// 0.1000000000000000055511151231257827021181583404541015625

System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));
// 0.3

SQL splits by column type rather than by dialect:

-- PostgreSQL: a bare decimal literal is NUMERIC, which is exact
SELECT 0.1 + 0.2 = 0.3;                          -- t

-- Cast to binary floating point and equality fails
SELECT 0.1::float8 + 0.2::float8 = 0.3::float8;  -- f

-- SQLite: REAL is a double, and the printed value hides it
SELECT 0.1 + 0.2;        -- 0.3
SELECT 0.1 + 0.2 = 0.3;  -- 0  (false)

The SQLite pair is worth a second look. The printed result says 0.3, the comparison says otherwise, and both are correct.

Comparing Floats: Why == Fails and Number.EPSILON Isn’t a Tolerance

0.1 + 0.2 === 0.3 is false because the left side is a different double. The usual advice is to compare with a tolerance instead, and the most-repeated version of that advice is wrong:

Math.abs(a - b) < Number.EPSILON;   // ⚠️ not a general-purpose comparison

Number.EPSILON, the machine epsilon for doubles, is 2.220446049250313e-16, which is 2⁻⁵². MDN defines it as the difference between 1 and the smallest double greater than 1. That makes it the grid spacing at 1.0, not a universal error budget. Float spacing doubles at every power of two, so a constant threshold is wrong everywhere except the neighbourhood where it was measured.

Near 1.0 it happens to work:

Math.abs((0.1 + 0.2) - 0.3);                  // 5.551115123125783e-17
Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON; // true

Scale the same computation up by a billion and it collapses:

const a = (0.1 + 0.2) * 1e9;
const b = 0.3 * 1e9;

a === b;                          // false
Math.abs(a - b);                  // 5.960464477539063e-8
Math.abs(a - b) < Number.EPSILON; // false — yet a and b are adjacent doubles

Around 1e9 the spacing between neighbouring doubles is roughly 1.19e-7. Since 1e9 falls in [2²⁹, 2³⁰), the ULP there is 2²⁹⁻⁵² = 2⁻²³ ≈ 1.19e-7, about 500 million times larger than Number.EPSILON. Any real rounding error at that magnitude dwarfs the threshold, so the check returns false forever. Down at 1e-20 the same constant is far too generous and declares obviously different values equal.

Absolute, relative, and ULP tolerance

Three options, each with a different failure mode:

  • Absolute tolerance, |a − b| <= atol, is right when you know the magnitude in advance. It is also the only one that works against zero, since a relative tolerance around 0 is always 0.
  • Relative tolerance, |a − b| <= rtol × max(|a|, |b|), scales with the data across magnitudes but degenerates near zero.
  • ULP distance counts how many representable values sit between the two bit patterns. It is the most precise measure and the least readable one.

Combine the first two and you get the form most numerics libraries settle on:

function nearlyEqual(a, b, rtol = 1e-9, atol = 1e-12) {
  if (a === b) return true;            // handles Infinity === Infinity
  const diff = Math.abs(a - b);
  return diff <= Math.max(rtol * Math.max(Math.abs(a), Math.abs(b)), atol);
}

nearlyEqual(0.1 + 0.2, 0.3);                  // true
nearlyEqual((0.1 + 0.2) * 1e9, 0.3 * 1e9);    // true
nearlyEqual(0, 1e-15);                        // true
nearlyEqual(1, 1.0001);                       // false

Python ships this in the standard library, and NumPy’s allclose uses the same formula:

import math
math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9, abs_tol=1e-12)  # True
math.isclose(1e9, 1e9 + 1e-7, rel_tol=1e-9)                # True

For the strict version, map the bits to a monotonic integer ordering and subtract:

const view = new DataView(new ArrayBuffer(8));

function toOrdinal(x) {
  view.setFloat64(0, x);
  const i = view.getBigInt64(0);
  return i >= 0n ? i : -(1n << 63n) - i;   // make negatives order correctly
}

function ulpDistance(a, b) {
  const d = toOrdinal(a) - toOrdinal(b);
  return d < 0n ? -d : d;
}

ulpDistance(0.1 + 0.2, 0.3);                // 1n
ulpDistance((0.1 + 0.2) * 1e9, 0.3 * 1e9);  // 1n
ulpDistance(-0, 0);                         // 0n

Exact == is not always wrong, by the way. It is fine for integer-valued doubles below 2⁵³, for constants you assigned and never computed on, and for checking whether a value is exactly zero.

When Precision Costs Money: Choosing the Right Numeric Type

Currency and binary floats are a bad match, and not because any single error is large. The errors are systematic and reproducible, and they stay invisible until an auditor finds them:

19.99 * 100;              // 1998.9999999999998
Math.round(19.99 * 100);  // 1999

(1.005).toFixed(2);       // '1.00'  — 1.005 is stored as 1.00499999999999989...

That second one catches teams every year. toFixed did not round wrong. The value handed to it was already below 1.005.

Integer minor units

Store cents, not dollars. 1999 means $19.99, arithmetic runs on integers, and you divide only at the moment of display.

const priceCents = 1999;             // $19.99
const subtotal = priceCents * 3;     // 5997 — exact

const totalCents = 1000;             // $10.00 split three ways
const share = Math.floor(totalCents / 3);   // 333
const remainder = totalCents - share * 3;   // 1 cent to allocate

Two caveats. Above Number.MAX_SAFE_INTEGER (9007199254740991, or 2⁵³ − 1) JavaScript integers stop being exact, so use BigInt. And integers do not decide your rounding policy: division, percentages, and tax splits still need an explicit rule for where the leftover cent goes.

Decimal types

A decimal type stores base-10 digits, so decimal fractions land exactly. Construct it from a string, never from a float, or you inherit the binary error before you start:

from decimal import Decimal

Decimal("0.1") + Decimal("0.2") == Decimal("0.3")   # True
Decimal(0.1)                                        # 0.1000000000000000055511151231257827021181583404541015625
Console.WriteLine(0.1m + 0.2m);          // 0.3
Console.WriteLine(0.1m + 0.2m == 0.3m);  // True

Java has BigDecimal, C# a native 128-bit decimal, SQL a NUMERIC(12,2). All of them fix decimal fractions. None fixes 1/3, because a decimal type is still a floating-point format; it just moved the base from 2 to 10.

JavaScript ships no decimal type, so the equivalent is a library. decimal.js gives arbitrary precision and every rounding mode, big.js does the same job with a much smaller API and bundle, and dinero.js skips general arithmetic to model money directly: an integer amount, a currency, and an allocation routine for the leftover cent.

Decision matrix

Use caseRecommended typeWhy
Money, billing, taxInteger minor units or decimalExact decimal arithmetic, auditable rounding
Scientific, physicsdouble (FP64)15–16 digits is plenty; best library support
Geometry, graphicsfloat or double + toleranceAlready approximate; compare with epsilon
ML trainingbfloat16FP32’s range at half the memory
ML inference, storageFP16More mantissa bits when values are normalised
Counters, IDs, keys64-bit integer or BigIntNever belonged in a float

Error Accumulation and Catastrophic Cancellation

One rounding error is 1e-17 and harmless. Ten thousand of them are a support ticket:

let total = 0;
for (let i = 0; i < 10000; i++) total += 0.1;

total;         // 1000.0000000001588
total - 1000;  // 1.588205122970976e-10

Drift grows roughly with the number of operations. In a loop this size it is invisible; in a nightly aggregation over millions of rows it is not.

Catastrophic cancellation

The nastier failure is subtraction. Subtract two nearly equal numbers and their matching leading digits cancel, leaving a result dominated by whatever error had already accumulated. The absolute error does not grow, but the relative error explodes, because the result is now tiny while the error stayed the same size.

The textbook one-pass variance formula, E[x²] − (E[x])², walks straight into it:

const data = [1e9, 1e9 + 1, 1e9 + 2];
const mean = data.reduce((s, x) => s + x, 0) / data.length;

// One-pass: E[x²] − (E[x])²
data.reduce((s, x) => s + x * x, 0) / data.length - mean * mean;  // 0

// Two-pass: centre the data first
data.reduce((s, x) => s + (x - mean) ** 2, 0) / data.length;      // 0.6666666666666666

The true population variance is 2/3. The one-pass formula returns exactly zero. Both operands were around 1e18, and the difference between them lives below the last stored bit, so the subtraction wipes out the whole signal instead of shaving a few digits off it. Welford’s online algorithm avoids this by updating the mean and the sum of squared deviations incrementally.

The quadratic formula has the same weakness when b² ≫ 4ac, and the same style of fix:

const a = 1, b = 1e8, c = 1;
const disc = Math.sqrt(b * b - 4 * a * c);

(-b + disc) / (2 * a);   // -7.450580596923828e-9  — about 25% wrong
(2 * c) / (-b - disc);   // -1e-8                  — correct

Both lines compute the same root. The first subtracts two nearly equal numbers; the second rearranges the algebra so it never has to. Goldberg’s What Every Computer Scientist Should Know About Floating-Point Arithmetic is still the reference treatment of cancellation and error bounds.

Kahan summation

Kahan’s compensated summation keeps a running record of the low-order bits each addition throws away, then feeds them back on the next iteration:

function kahanSum(values) {
  let sum = 0;
  let compensation = 0;
  for (const value of values) {
    const y = value - compensation;
    const t = sum + y;
    compensation = (t - sum) - y;   // the bits that fell off
    sum = t;
  }
  return sum;
}

kahanSum(new Array(10000).fill(0.1));   // 1000  — exactly

Python gives you this for free. math.fsum([0.1] * 10_000) returns 1000.0, and since CPython 3.12 the built-in sum() uses Neumaier compensation too, so sum([0.1] * 10_000) is exact while an explicit += loop still drifts to 1000.0000000001588.

Reach for compensation on long sums, financial aggregates, and numerical integration. Skip it for a handful of values or when you have already moved to an exact type.

The Special Values That Break Assumptions

IEEE 754 reserves bit patterns for values that do not obey the rules you expect:

NaN === NaN;          // false — mandated by the standard
Number.isNaN(NaN);    // true
[NaN].indexOf(NaN);   // -1   (uses ===)
[NaN].includes(NaN);  // true (uses SameValueZero)

-0 === 0;             // true
Object.is(-0, 0);     // false
1 / -0;               // -Infinity

1e308 * 10;           // Infinity — overflow never throws
Infinity - Infinity;  // NaN
Number.MIN_VALUE;     // 5e-324 — the smallest subnormal double

NaN compares unequal to everything including itself, by design, so a failed computation can never masquerade as a valid result. Test it with Number.isNaN() or Python’s math.isnan().

Overflow is quieter and more dangerous: it returns ±Infinity and keeps going until someone downstream notices a chart full of blanks. Negative zero is a distinct bit pattern that still equals +0 under ===; it appears from underflow of a negative value or from -1 * 0, and only division or Object.is reveals it.

Subnormals fill the gap between zero and the smallest normal number. With the exponent field all zeros the format drops the implicit leading 1, and the mantissa shrinks gradually toward zero. That guarantees the difference of two unequal floats never rounds to exactly zero. The price is falling precision and, on some hardware, a sharp performance cliff. The special-value chips in the IEEE 754 converter load ±0, ±Infinity, NaN, and the smallest subnormal so you can inspect each bit pattern directly.

float vs double vs FP16 vs bfloat16

All four formats follow the same standard and differ only in how they split their bits between range and floating point precision:

FormatTotal bitsExponentMantissaApprox decimal digitsMax finiteTypical use
binary16 (FP16)16510~3.365504GPU inference, compact storage
bfloat161687~2.4~3.39 × 10³⁸ML training
binary32 (float)32823~7.2~3.40 × 10³⁸Graphics, sensors, GPU
binary64 (double)641152~15.9~1.80 × 10³⁰⁸Default everywhere else

Exponent bits buy range; mantissa bits buy precision. FP16 spends its budget on precision and caps out at 65504, close enough to real gradient values that overflow to Infinity is a routine hazard. bfloat16 makes the opposite trade: it keeps FP32’s eight exponent bits and lives with seven mantissa bits, so values that would overflow FP16 sail through and training rarely needs loss scaling.

Default to double. Drop to a narrower format after you have measured a memory or bandwidth bottleneck, and check what the narrowing costs by entering the same value in each format in the converter.

Practical Rules for Floating Point Precision

  • Never use == on computed floats. Use a combined absolute-plus-relative tolerance sized to your data.
  • Do not treat Number.EPSILON as a threshold. It describes the grid at 1.0 and nothing else.
  • Keep money in integer minor units or a decimal type, and always build decimals from strings.
  • Compensate long sums with Kahan, Neumaier, or math.fsum.
  • Rearrange formulas that subtract near-equal quantities rather than tightening tolerances around them.
  • Format explicitly for humans with toFixed, an f-string, or printf. Never ship a default repr to users.
  • Send numbers across service boundaries as strings or integers. A JSON round-trip through a double is silent and lossy.
  • Pick NUMERIC, not FLOAT, for currency columns. Fixing this after launch means a migration and a reconciliation.
  • Think in bits when a value looks impossible. The same habit powers our bitwise operations guide, and it turns floating-point mysteries into arithmetic you can check.

FAQ

Is floating point math broken?

Floating point math is not broken; it follows IEEE 754 exactly. The standard represents real numbers in a finite number of binary digits, and decimal fractions such as 0.1 have no exact binary form, so the nearest representable value is stored. The broken part is the expectation that every decimal fits.

Why does Python print 0.1 as 0.1 but 0.1 + 0.2 as 0.30000000000000004?

Python’s repr uses shortest round-trip formatting: it prints the shortest decimal string that parses back to the identical double. "0.1" already identifies its double uniquely. The sum 0.1 + 0.2 is a different double from the one "0.3" maps to, so the formatter must emit all seventeen digits to stay unambiguous.

Why shouldn’t I use Number.EPSILON to compare two floats?

Number.EPSILON (about 2.22e-16) is the gap between 1 and the next double, not a universal error budget. Float spacing doubles at every power of two, so near 1e9 neighbouring doubles are about 1.19e-7 apart. Any genuine rounding error there exceeds EPSILON, making the comparison always false. Use a relative or combined tolerance.

Can I use floating point for money if I round at the end?

Rounding at the end does not rescue floating point money. Errors accumulate across additions, multiplications, and multi-step allocations, and the moment you round changes the total. 19.99 * 100 already yields 1998.9999999999998. Auditing needs reproducible exact arithmetic: integer cents, decimal, BigDecimal, or SQL NUMERIC.

Which decimal numbers can be stored exactly in a double?

A double stores exactly only values expressible as m / 2ⁿ for integers m and n within the format’s precision. That covers 0.5, 0.25, 0.125, 0.75, and 2.5, plus every integer up to 2⁵³. Reduce the fraction first: any remaining factor of 5 in the denominator, as in 0.1, 0.2, 0.3, or 0.7, means the value is approximated.

Why is 0.1 + 0.2 === 0.3 false but 0.5 + 0.25 === 0.75 true?

0.1 + 0.2 === 0.3 is false because its operands are rounded, while 0.5, 0.25, and 0.75 are 2⁻¹, 2⁻², and 2⁻¹ + 2⁻², all exactly representable, so their sum needs no rounding. 0.1, 0.2, and 0.3 are each stored approximately, and rounding the sum of the first two lands one ULP above the double that 0.3 maps to.

Does this happen in every programming language?

Every language built on IEEE 754 binary floating point shows the same behaviour: JavaScript, Python, Java, C, C++, Go, Rust, Swift, and SQL’s FLOAT. What differs is the default print precision and whether an exact decimal type ships in the box, such as C#‘s decimal or Python’s decimal module.

How do I round a number to 2 decimal places without floating point errors?

To round to 2 decimal places without floating point errors, format at the point of display with Intl.NumberFormat and keep money in integer cents or a decimal type. Math.round(n * 100) / 100 inherits the input error: 1.005 * 100 is 100.49999999999999, so it returns 1, not 1.01, exactly as (1.005).toFixed(2) gives '1.00'. toFixed also returns a string.

How do I compare floats in unit tests?

Compare floats in unit tests with the framework’s tolerance assertion rather than ==. Python has pytest.approx, which defaults to a relative tolerance. Jest and Vitest offer toBeCloseTo, which counts decimal places, not relative error, so it tightens as values grow. JUnit takes an explicit delta: assertEquals(expected, actual, delta). Size tolerances by magnitude, as the absolute, relative, and ULP section describes.

Tags: floating-point ieee-754 javascript python numbers

Related Articles

View all articles