Chapter 3.2☕ 20 min read

Numbers: IEEE 754 & Why 0.1 + 0.2 ≠ 0.3

0.1 + 0.2 = 0.30000000000000004 — bug nahi hai bhai, physics hai.

0101 — The 64-Bit Float Box: Sign, Exponent, Mantissa

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!
IEEE 754 was created in 1985. JS adopted it in 1995 — the ONLY numeric type. Java has int, long, float, double. C has them all. JS has ONE — the 64-bit float. This is why integers up to 2^53 are exact but beyond that, precision is lost. This is NOT a JS choice — it is the IEEE 754 standard used by C, Java, Python, and virtually every other language.
0202 — Why 0.1 Cannot Be Stored Exactly: Binary Fractions

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
📋 Golden Rule:
• 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.
0303 — Special Values: NaN, Infinity, -0

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) — NOT isNaN(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 === 0 is true in JS! But Object.is(-0, 0) is false
  • 1/-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
NaN !== NaN is the single most surprising JS behavior for new developers. It comes directly from the IEEE 754 standard — the spec says NaN is never equal to anything, including itself. This is WHY Array.prototype.includes correctly finds NaN (uses SameValueZero) but indexOf does not (uses ===).
0404 — MAX_SAFE_INTEGER: When Integers Lose Precision

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!
0505 — Practical Fixes: Safe Number Operations

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 comparisonMath.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"
📋 Golden Rule:
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
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase