why 0.1 + 0.2 is not 0.3
The console prints 0.30000000000000004, a price comparison fails, a sum of invoice line items is off by one cent. Floating point precision is not a JavaScript quirk, it is a consequence of IEEE 754, but it hits JavaScript developers especially often because the language only has a single number type. This article explains the cause and shows robust fixes.
Table of Contents
- 1. Why 0.1 + 0.2 is not 0.3
- 2. IEEE 754: how JavaScript actually stores numbers
- 3. Tolerance-based comparisons: using Number.EPSILON correctly
- 4. Rounding errors in money amounts: the most expensive pitfall
- 5. toFixed and its own pitfalls
- 6. Number.isInteger and safe integer limits
- 7. BigInt as a way out for integer precision
- 8. Libraries and alternatives for exact decimal arithmetic
- 9. Approaches against floating point precision errors compared
- 10. Summary
- 11. FAQ
1. Why 0.1 + 0.2 is not 0.3
Hardly any code example demonstrates floating point precision as vividly as 0.1 + 0.2 in the JavaScript console, the result reads 0.30000000000000004 instead of the expected 0.3. For developers running into this for the first time, it looks like a bug in the language, but in fact it is a direct consequence of how computers represent decimal numbers in binary. JavaScript's floating point precision is not a quirk of the language itself, it follows the IEEE 754 standard, the same standard Java, Python, C, and practically every other modern language uses for its default number type.
The decisive difference from many other languages is that JavaScript has only a single primitive numeric type, the 64-bit floating point type double from IEEE 754. There is no separate int type in JavaScript, no built in decimal arithmetic without an extra library, and no way to sidestep floating point precision for simple integer calculations without deliberately switching to another data type such as BigInt. As a result, JavaScript developers run into floating point precision problems earlier and more often than developers in languages with separate integer and decimal types.
The following sections first explain the technical cause through IEEE 754, then show robust comparison and rounding strategies, and finally cover BigInt and external libraries as complete solutions for exact arithmetic.
2. IEEE 754: how JavaScript actually stores numbers
The IEEE 754 standard represents a number through three components: a sign bit, an exponent, and a mantissa, sixty four bits in total for the double precision type JavaScript uses for every number. This binary representation can exactly represent every integer within its range, but many decimal fractions cannot be represented exactly, for the same reason that one third cannot be written as a finite decimal fraction in the decimal system. 0.1 and 0.2 fall exactly into this category: their binary representation is an infinitely repeating fraction that must be cut off at 64 bits, which already creates a tiny rounding error before the actual addition even happens.
When two already rounded values are added, their tiny rounding errors add up too, and the result deviates from the exact result by an amount that is visible to humans, even though mathematically tiny. This floating point precision issue affects not just addition, but every operation involving decimal fractions that cannot be represented exactly in binary, including subtraction, multiplication and division. It is important to understand that this is not a bug in the JavaScript engine, it is an unavoidable mathematical property of binary floating point representation itself.
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
// Not unique to addition — subtraction and multiplication show it too
console.log(0.3 - 0.1); // 0.19999999999999998
console.log(0.1 * 3); // 0.30000000000000004
// The exact IEEE 754 representation, shown with enough decimal digits
console.log((0.1).toFixed(20)); // 0.10000000000000000555
console.log((0.2).toFixed(20)); // 0.20000000000000001110
// Both are already rounded before the addition even happens
3. Tolerance-based comparisons: using Number.EPSILON correctly
Because floating point precision errors make direct equality comparisons like 0.1 + 0.2 === 0.3 unreliable, comparing two calculated floating point numbers directly with === is fundamentally risky as soon as at least one of the numbers comes from a calculation involving decimal fractions that cannot be represented exactly in binary. The established fix is a tolerance based comparison, checking whether the absolute difference between two values is smaller than a very small threshold, instead of demanding exact equality.
Number.EPSILON provides exactly this threshold as the smallest representable difference between 1 and the next larger representable number, and it works as a starting point for a tolerance comparison for numbers around the magnitude of one. For numbers that are significantly larger or smaller, the threshold must be scaled accordingly, because the absolute size of the rounding error grows with the magnitude of the numbers involved. A blanket, hardcoded tolerance value with no relation to the actual magnitude of the numbers therefore introduces new, subtle bugs of its own.
// WRONG: exact equality fails due to floating point precision
console.log(0.1 + 0.2 === 0.3); // false
// RIGHT: tolerance-based comparison using Number.EPSILON
function nearlyEqual(a, b, epsilon = Number.EPSILON * 8) {
return Math.abs(a - b) < epsilon;
}
console.log(nearlyEqual(0.1 + 0.2, 0.3)); // true
// For larger magnitudes, scale the tolerance relative to the values
function nearlyEqualScaled(a, b, relativeEpsilon = 1e-10) {
const diff = Math.abs(a - b);
const scale = Math.max(Math.abs(a), Math.abs(b), 1);
return diff <= relativeEpsilon * scale;
}
console.log(nearlyEqualScaled(1000000.1 + 0.2, 1000000.3)); // true
4. Rounding errors in money amounts: the most expensive pitfall
The economically most expensive pitfall of floating point precision arises when money amounts are stored and added directly as floating point numbers in euros or dollars. A shopping cart with many line items of 0.10 or 0.20 units each accumulates exactly the same tiny rounding errors as in the introductory example, and by the end of a long invoice sum, a difference of one cent can appear, which shows up as a discrepancy in accounting systems and triggers manual investigation.
The established fix in the financial industry is to store money amounts internally not in the main currency unit but in the smallest subunit as an integer, in euros that means as a cent amount. Integers within JavaScript's safe integer range are represented exactly by IEEE 754, so floating point precision errors completely disappear when adding and subtracting cent amounts. Only at the final display step is the cent amount divided by 100 and formatted for the user, so no intermediate calculation ever works with fractions of the main currency.
// WRONG: accumulating floating point precision errors across many items
const pricesInEuro = [19.99, 5.5, 3.33, 0.99];
const totalWrong = pricesInEuro.reduce((sum, price) => sum + price, 0);
console.log(totalWrong); // 29.81 in this case, but can drift with more items
// RIGHT: store money as integer cents, no fractional main unit ever appears
const pricesInCents = [1999, 550, 333, 99];
const totalCents = pricesInCents.reduce((sum, cents) => sum + cents, 0);
console.log(totalCents / 100); // 29.81 — reliably exact, formatted only at the end
function formatEuro(cents) {
return (cents / 100).toLocaleString('en-US', { style: 'currency', currency: 'EUR' });
}
console.log(formatEuro(totalCents)); // "€29.81"
5. toFixed and its own pitfalls
toFixed() is often reached for as a quick fix against floating point precision problems, but it has its own quirks that cause surprises. The method always returns a string, never a number, which leads to implicit type coercion and subtle bugs in subsequent arithmetic if the return value is not explicitly converted back into a number. On top of that, toFixed() does not always round according to the classic commercial rounding rule, because the underlying floating point precision of the input number affects the rounding result here as well.
A concrete example: (1.005).toFixed(2) returns "1.00" in most engines instead of the expected "1.01", because 1.005 is actually stored in binary floating point representation as a slightly smaller number, which tips the rounding downward. Anyone who needs reliable commercial rounding should either switch to whole subunits as in the previous section, or use a dedicated rounding function that guards the input value against floating point precision by multiplying and explicitly correcting before rounding.
// toFixed() returns a string, not a number — easy to forget
const price = 19.5;
console.log(typeof price.toFixed(2)); // "string", not "number"
console.log(price.toFixed(2) + 1); // "19.501" — string concatenation, not addition!
// Floating point precision can make toFixed() round the "wrong" way
console.log((1.005).toFixed(2)); // "1.00" — not "1.01" as naively expected
// Reason: 1.005 is actually stored as ~1.00499999999999989...
// A more robust rounding helper for two decimal places
function roundToCents(value) {
return Math.round((value + Number.EPSILON) * 100) / 100;
}
console.log(roundToCents(1.005)); // 1.01 — corrects for the representation error
6. Number.isInteger and safe integer limits
Even pure integer calculations are not unlimited safe, because the 64-bit floating point type can only represent every integer exactly up to a certain limit. Number.MAX_SAFE_INTEGER sits at 2 to the power of 53 minus 1, because the mantissa of the IEEE 754 format provides 53 bits for representing integers. Above this limit, several different mathematical integers can map onto the same floating point value, which makes comparisons and additions silently produce wrong results without ever throwing an error.
Number.isSafeInteger() explicitly checks whether a value lies within this safe limit and is actually an integer, which matters especially when processing IDs from external systems, for example database IDs or snowflake IDs that are frequently transmitted in JSON as plain numbers rather than strings. If such an ID above the safe integer limit is parsed as a JavaScript number, the value can silently change due to floating point precision, with the consequence that records get queried using the wrong ID.
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.isSafeInteger(2 ** 53)); // false — right above the safe limit
// Precision loss above the safe integer range — a real-world API pitfall
const idFromApi = 9007199254740993; // JSON often sends large IDs as plain numbers
console.log(idFromApi); // 9007199254740992 — already wrong, silently
// Guard before trusting a numeric ID from an external source
function assertSafeId(value) {
if (!Number.isSafeInteger(value)) {
throw new Error(`ID ${value} exceeds safe integer precision — use a string or BigInt`);
}
return value;
}
7. BigInt as a way out for integer precision
BigInt solves exactly the problem described in the previous section for integer values, by representing integers of any size exactly, independent of the 53-bit limit of the regular Number type. A BigInt literal is marked with the n suffix, for example 9007199254740993n, and every arithmetic operation between two BigInt values stays exact, with no floating point precision issue whatsoever, because BigInt is not based on IEEE 754 internally, it implements arbitrary length integer arithmetic instead.
The limitation of BigInt is that it can only represent integers, no fractional part, and that it cannot be mixed with regular Number values without explicit conversion, a direct comparison using + between BigInt and Number throws a TypeError. For use cases with very large integers, such as cryptographic calculations, database IDs, or counters that can exceed the safe integer limit, BigInt is nevertheless the most robust native solution, with no external library and no floating point precision loss whatsoever.
const bigId = 9007199254740993n; // BigInt literal, exact even above MAX_SAFE_INTEGER
console.log(bigId + 1n); // 9007199254740994n — exact, no precision loss
// Mixing BigInt and Number directly throws
try {
console.log(bigId + 1); // TypeError: Cannot mix BigInt and other types
} catch (error) {
console.log(error.message);
}
// Explicit conversion is required in both directions
console.log(bigId + BigInt(1)); // 9007199254740994n
console.log(Number(bigId) + 1); // precision loss reappears once converted back
8. Libraries and alternatives for exact decimal arithmetic
For use cases that need both exact integer and exact decimal arithmetic, for example financial systems with interest calculations or scientific applications, neither the native cent approach nor BigInt alone is enough, because both are either limited to integers or require manual scaling. For these cases, dedicated decimal arithmetic libraries exist, such as decimal.js or big.js, which represent numbers internally as string based, arbitrary precision decimal values and thereby completely avoid any floating point precision issue of native Number arithmetic.
The downside of these libraries is the extra computational overhead compared to native floating point operations, because every operation runs through string parsing and its own arithmetic implementation, instead of accessing hardware floating point instructions directly. For the vast majority of web applications, this overhead is negligible compared to the risk of incorrect financial calculations, and using such a library pays off whenever calculations require more than two decimal places, complex rounding rules, or a very large number of addition steps.
// Conceptual example using a decimal arithmetic library (e.g. decimal.js)
// import Decimal from 'decimal.js';
// const a = new Decimal('0.1');
// const b = new Decimal('0.2');
// console.log(a.plus(b).toString()); // "0.3" — exact, no floating point precision loss
// Native alternative for two-decimal money math without a dependency:
function addMoney(...centsValues) {
return centsValues.reduce((sum, cents) => sum + cents, 0);
}
console.log(addMoney(1999, 550, 333, 99) / 100); // 29.81, exact every time
9. Approaches against floating point precision errors compared
Depending on the use case, different approaches against floating point precision errors fit best, and the choice depends heavily on whether integers, simple money amounts, or complex decimal arithmetic are the main concern.
| Use case | Risk | Recommended approach | Limits |
|---|---|---|---|
| Number comparison | === fails | Number.EPSILON tolerance | Scale the threshold by magnitude |
| Money amounts | Cent-level drift in totals | Integer cents instead of floating point | Format only at display time |
| Large IDs | Precision loss above 2^53 | BigInt | No fractional part possible |
| Complex decimal arithmetic | Accumulated rounding errors | decimal.js or big.js | Extra computational overhead |
| Display with fixed decimals | toFixed sometimes rounds wrong | Round before toFixed, EPSILON correction | Convert the string return value explicitly |
The table shows there is no single, universal fix for floating point precision errors, the right approach depends on the concrete use case. Anyone who treats money amounts, large IDs, and simple numeric comparisons differently, instead of blanket trusting native floating point arithmetic, avoids the vast majority of bugs that occur in practice.
Mironsoft
JavaScript debugging, code reviews and frontend architecture
Are rounding errors hitting your financial calculations?
We audit existing code for risky floating point arithmetic in money amounts and IDs, and migrate calculations to integer cents, BigInt, or dedicated decimal arithmetic libraries.
Code Review
Targeted search for risky floating point comparisons and sums
Financial Refactoring
Migrating money amounts to an integer cent representation
Library Selection
Choosing and integrating decimal.js or big.js for your requirements
10. Summary
Floating point precision errors in JavaScript are not a language quirk, they are an unavoidable consequence of the IEEE 754 standard, under which many decimal fractions cannot be represented exactly in binary. 0.1 + 0.2 therefore results in 0.30000000000000004, because the individual operands are already stored rounded before the addition even takes place. Direct equality comparisons using === are therefore fundamentally risky for calculated floating point numbers and should be replaced with a tolerance based comparison using Number.EPSILON.
For money amounts, the most robust fix is to consistently calculate internally with integer cents instead of the main currency unit, because integers within the safe range are represented exactly. For very large integers beyond the safe limit, BigInt provides a remedy, and for complex decimal arithmetic with many decimal places, dedicated libraries like decimal.js offer the most reliable solution. Anyone who deliberately applies these three tools depending on the use case avoids the vast majority of floating point precision errors in practice.
Floating Point Precision in JavaScript, the essentials at a glance
Cause
IEEE 754 cannot represent many decimal fractions exactly in binary, the same reason one third has no finite decimal form.
Comparisons
Never use === for calculated floating point numbers. Use a tolerance comparison with Number.EPSILON.
Money
Always store integer cents internally, format to the main currency unit only at display time.
Large Numbers
BigInt for integers above 2^53, decimal.js or big.js for complex decimal arithmetic.