Numbers: IEEE 754 & Why 0.1 + 0.2 ≠ 0.3
0.1 + 0.2 = 0.30000000000000004 — bug nahi hai bhai, physics hai.
Every JavaScript number — whether 42, 3.14, or 0.1 — is stored as a 64-bit IEEE 754 double-precision floating point. There is no separate integer type in JS. Every number gets the same 64-bit box.
The 64 bits are split into three parts:
- Sign bit (1 bit): 0 = positive, 1 = negative
- Exponent (11 bits): determines the magnitude range. Stored as biased integer (actual exponent + 1023)
- Mantissa (52 bits): the significant digits — this is where precision lives
Total range: ~5e-324 (Number.MIN_VALUE) to ~1.8e308 (Number.MAX_VALUE). Precision: ~15-17 significant decimal digits.
// The 64-bit layout (conceptual):
// [S][EEEEEEEEEEE][MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM]
// 1 bit 11 bits 52 bits
// sign exponent mantissa (fraction)
// You can inspect bits using DataView:
function getBits(num) {
const buf = new ArrayBuffer(8);
const view = new DataView(buf);
view.setFloat64(0, num, false); // big-endian
let bits = "";
for (let i = 0; i < 8; i++) {
bits += view.getUint8(i).toString(2).padStart(8, "0");
}
return bits;
}
console.log(getBits(1)); // 0 01111111111 0000...0000
console.log(getBits(-1)); // 1 01111111111 0000...0000
console.log(getBits(0.5)); // 0 01111111110 0000...0000
console.log(getBits(0.1)); // 0 01111111011 1001100110011...
// Notice 0.1 has a REPEATING pattern in the mantissa!
Here is the fundamental truth that confuses every developer the first time:
In decimal, 1/3 = 0.333... — the 3s go on forever. You cannot write 1/3 as an exact decimal. The same thing happens in binary with 0.1:
Binary fractions that terminate: 0.5 = 1/2, 0.25 = 1/4, 0.125 = 1/8, 0.0625 = 1/16
0.1 in binary: 0.0001100110011001100110011... — the pattern "0011" repeats forever, just like "3" repeats in 1/3.
You cannot fit an infinite repeating sequence into 52 mantissa bits. It gets truncated. That truncation is the source of the error.
// The fundamental truth:
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
// See the full precision:
console.log((0.1).toPrecision(55));
// 0.1000000000000000055511151231257827021181583404541015625
console.log((0.2).toPrecision(55));
// 0.2000000000000000111022302462515654042363166809082031250
console.log((0.3).toPrecision(55));
// 0.2999999999999999888977697537484345957636833190917968750
// These ARE exact — binary fractions that terminate
console.log(0.5 + 0.25); // 0.75 — exact!
console.log(0.5 + 0.5); //1 — exact!
console.log(0.125 + 0.375); // 0.5 — exact!
// Powers of 2 in fractions are always exact
// How many decimals can you trust?
console.log(0.1 + 0.2); // 0.30000000000000004
console.log((0.1 + 0.2).toFixed(1)); // "0.3" — rounds for display
console.log(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON); // true — safe comparison
• 0.1 + 0.2 ≠ 0.3 is true in EVERY language using IEEE 754 — Python, Java, C++, all get the same result. JS just shows more digits by default.
• The fix: never compare floats with ===. Use
Math.abs(a - b) < Number.EPSILON for equality, or multiply to integers (cents instead of dollars), or use a decimal library.IEEE 754 defines special bit patterns for values that are not "normal" numbers:
NaN (Not a Number): Result of invalid math operations. IEEE 754 defines many NaN bit patterns; JS uses one.
NaN !== NaN— the only JS value not equal to itself (IEEE 754 spec requirement)typeof NaN === 'number'— it IS a number type, just a special one- Check:
Number.isNaN(x)— NOTisNaN(x)(isNaN coerces first!)
Infinity: Exponent all 1s, mantissa all 0s — special bit pattern.
1/0 = Infinity(not an error in IEEE 754),-1/0 = -Infinity
-0 (Negative Zero): Sign bit = 1, everything else = 0.
-0 === 0is true in JS! ButObject.is(-0, 0)is false1/-0 = -Infinity(sign matters for division)
// NaN — Not a Number (but typeof 'number'!)
console.log(0 / 0); // NaN
console.log(Math.sqrt(-1)); // NaN
console.log(parseInt('hello'));// NaN
console.log(typeof NaN); // 'number' — confusing but correct
// The unique self-inequality
console.log(NaN === NaN); // false — ONLY value in JS not equal to itself
console.log(NaN !== NaN); // true
// Correct NaN check:
console.log(Number.isNaN(NaN)); // true ✅
console.log(Number.isNaN('hello')); // false ✅ — doesn't coerce
console.log(isNaN('hello')); // true ❌ — coerces string to NaN first!
// Infinity
console.log(1 / 0); // Infinity
console.log(-1 / 0); // -Infinity
console.log(Infinity + 1); // Infinity
console.log(Infinity - Infinity); // NaN!
// Negative zero — the sneaky one
console.log(-0 === 0); // true — they're "equal"!
console.log(1 / -0); // -Infinity — sign preserved in division
console.log(Object.is(-0, 0));// false — strict identity check
console.log(String(-0)); // "0" — toString hides the sign
console.log(JSON.stringify(-0));// "0" — JSON also hides it
Array.prototype.includes correctly finds NaN (uses SameValueZero) but indexOf does not (uses ===).52 mantissa bits can represent 2^53 - 1 consecutive integers exactly. Beyond that, the gaps between representable floats grow.
Number.MAX_SAFE_INTEGER = 9007199254740991 (2^53 - 1). "Safe" means "guaranteed to be exactly representable".
Beyond this limit, not all integers are representable — gaps appear. 2^53 + 1 cannot be stored! (2^53 and 2^53 + 2 are fine, but +1 rounds).
Practical consequence: Large IDs from databases/APIs (like Twitter snowflake IDs or database primary keys) may lose precision when parsed as JS numbers.
BigInt (ES2020): Arbitrary precision integers — no float approximation. Created with the n suffix.
// Safe integer range
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.MAX_SAFE_INTEGER + 1); // 9007199254740992 — still ok
console.log(Number.MAX_SAFE_INTEGER + 2); // 9007199254740992 — SAME! Lost +2!
console.log(Number.MAX_SAFE_INTEGER + 3); // 9007199254740994
// The gap: 9007199254740993 cannot be represented!
console.log(9007199254740993 === 9007199254740992); // true — they're the same float!
// Real-world problem: large IDs from APIs
const apiResponse = { userId: 9007199254740993 }; // from JSON
// JSON.parse converts to JS number — precision lost!
// The ID you received ≠ the ID the server sent!
// BigInt — exact integers of any size
const big = 9007199254740993n; // note the 'n' suffix
console.log(big); // 9007199254740993n — exact!
console.log(big + 1n); // 9007199254740994n — exact!
console.log(typeof big); // 'bigint'
// BigInt limitations:
// console.log(big + 1); // TypeError — cannot mix BigInt and Number
// console.log(Math.sqrt(big));// TypeError — Math functions don't accept BigInt
const asNum = Number(big); // Convert — but you lose precision again!Now that you know WHY floating point behaves this way, here are the practical fixes used in production code:
- Fix 1: Integer arithmetic — multiply to cents/paise, work in integers, divide at display time.
- Fix 2: Number.EPSILON comparison —
Math.abs(a - b) < Number.EPSILON * Math.max(a, b) - Fix 3: toFixed() for display — NOT for calculation (returns a string!)
- Fix 4: BigInt for large integers — database IDs, cryptography, exact financial calculations
- Fix 5: Intl.NumberFormat for currency — handles rounding and formatting correctly
- Fix 6: decimal.js / big.js libraries — arbitrary precision decimals for serious financial software
// Fix 1: Work in integers (cents instead of dollars)
function addPrices(price1, price2) {
// price1 = 0.10, price2 = 0.20 (dollars)
const cents1 = Math.round(price1 * 100); // 10 cents — SMI
const cents2 = Math.round(price2 * 100); // 20 cents — SMI
return (cents1 + cents2) / 100; // 0.30 — back to dollars
}
console.log(addPrices(0.1, 0.2)); // 0.3 — exact!
// Fix 2: Epsilon comparison for equality
function almostEqual(a, b, epsilon = Number.EPSILON) {
return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b));
}
console.log(almostEqual(0.1 + 0.2, 0.3)); // true ✅
// Fix 3: toFixed for DISPLAY only — returns string!
const price = 0.1 + 0.2;
console.log(price.toFixed(2)); // "0.30" — for display
console.log(typeof price.toFixed(2)); // 'string' — NOT a number!
// Fix 4: BigInt for large IDs
async function fetchUser(id) {
// id comes from URL or DB as a large integer
const bigId = BigInt(id); // safe conversion from string
const url = '/api/users/' + bigId.toString(); // back to string for URL
return fetch(url).then(r => r.json());
}
// Fix 5: Intl.NumberFormat for currency
const formatter = new Intl.NumberFormat('en-IN', {
style: 'currency', currency: 'INR'
});
console.log(formatter.format(1234.5)); // "₹1,234.50"
•
toFixed() is display-only and rounds — never use it for calculation. toFixed(2) on 1.005 gives "1.00" not "1.01" due to float representation.• For currency math, always work in smallest unit (paise, cents) as integers, or use a dedicated decimal library for financial applications.
Lo kar liya — Key Points:
- ✅ Every JS number is a 64-bit IEEE 754 double: 1 sign bit + 11 exponent + 52 mantissa
- ✅ 0.1 in binary is an infinite repeating fraction — truncated to 52 bits — the stored value is approximate
- ✅ 0.1 + 0.2 ≠ 0.3 is NOT a JS bug — it is correct IEEE 754 arithmetic, same in Python, Java, C++
- ✅ NaN !== NaN — the only JS value not equal to itself. Use Number.isNaN(), never isNaN()
- ✅ -0 === 0 is true in JS. Use Object.is(-0, 0) to distinguish them
- ✅ Number.MAX_SAFE_INTEGER = 2^53 - 1. Beyond this, integers lose precision. Use BigInt for exact large integers
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login