Beyond Code & Karma · JavaScript Internals

Floating Point

The number you see is not the number that is stored. 0.1 + 0.2 is 0.30000000000000004 — not a bug, but the price of squeezing the infinite real line into 64 bits. Sign, exponent, mantissa, and the illusion of exactness.

Interactive Deep Dive · IEEE-754

A computer cannot hold most decimals.
It can only hold the nearest one.

We write numbers in base ten, where each fractional place is a power of ten — 0.1 is one tenth. A computer stores numbers in base two, where each fractional place is a power of two: a half, a quarter, an eighth. Some decimals land neatly there (0.5 is exactly 2⁻¹), but one tenth never does. In binary, 0.1 is 0.0001100110011… repeating forever — just like 1/3 is 0.333… forever in decimal. A double has only 52 mantissa bits, so the pattern is cut off and rounded to the nearest value it can hold.

The Secret: floating point is not "inaccurate." It is exactly as accurate as 64 bits allow — every stored value is precise, it is just the nearest representable neighbour to the decimal you typed. The error isn't randomness; it's rounding, and it is deterministic.
Sign — 1 bit
A single bit decides direction: 0 is positive, 1 is negative. Flip it and the magnitude is untouched — the cleanest field in the format.
Exponent — 11 bits
The scale — how far to shift the binary point. Stored bias-encoded: subtract 1023 to get the true power of two, so one field spans the astronomically large and the vanishingly small.
Mantissa — 52 bits
The significant digits — the fraction after an implied leading 1. 52 stored bits give 53 bits of precision, roughly 15–17 decimal digits. This is where the rounding happens.
The Gap
Representable doubles aren't evenly spaced — they cluster near zero and spread out for large numbers. Between two neighbours, nothing exists; your value snaps to the closer one.

Maya — the float as a study in illusion

Hindu Philosophy Parallel

In Vedic thought, maya is the veil of appearance: the world the senses report is not the world as it truly is — satya (the real) lies beneath the rendered form. Floating point is maya made mechanical. The 0.3 printed on screen is rupa — a name the eye accepts — while the bits in memory hold something a hair apart. The rounding step is the veil itself: the act that turns an infinite binary truth into a finite, believable appearance.

Rupa — the rendered decimal
(0.1).toString() returns "0.1" — the friendly form the senses accept. Appearance, not essence.
Satya — the stored binary
The 52 mantissa bits hold 0.10000000000000001 — the value that truly is, beneath the name.
The veil — rounding
Cutting the infinite pattern to 52 bits is the veil drawn over satya — the step that births the illusion.

"To write (0.1 + 0.2) === 0.3 is to trust the veil. To compare within an epsilon is to look past the form to the truth beneath it."

Reading a double, field by field

IEEE-754 is scientific notation in base two: a sign, a fraction, and an exponent. The whole standard fits in one formula —

value = (−1)sign × 1.mantissa × 2(exponent − 1023)

Step 1 — grab the raw bits

JavaScript never exposes a number's bits directly, but a DataView over an ArrayBuffer lets you write a double and read its raw 64 bits back — native precision, no libraries:

read-bits.js
// The CPU keeps a number as raw bits — DataView reads them, no libs.
const buf = new ArrayBuffer(8);
const dv  = new DataView(buf);

dv.setFloat64(0, 0.1);          // write the double into memory
const raw = dv.getBigUint64(0); // read the 64 raw bits back
raw.toString(16);                // "3fb999999999999a"

Step 2 — split and reconstruct

Shift and mask the three fields out, then rebuild the value. This is exactly what the visualizer below animates as you toggle bits:

decode.js
// Pull the three fields out with shifts and masks.
const sign = Number((raw >> 63n) & 1n);      // 1 bit
const exp  = Number((raw >> 52n) & 0x7ffn);  // 11 bits
const mant = raw & 0xfffffffffffffn;            // 52 bits

const value = (sign ? -1 : 1)
  * (1 + Number(mant) / 2 ** 52)     // implied leading 1.f
  * 2 ** (exp - 1023);              // un-bias the exponent

The four moves to decode any double

Read the sign

The top bit. 0 → positive, 1 → negative. Multiply the result by (−1)^sign.

Un-bias the exponent

Read 11 bits as an unsigned integer, then subtract the 1023 bias to get the true power of two — the scale.

Rebuild the significand

Read 52 mantissa bits as a fraction and prepend the implied leading 1 → a value in [1, 2) called 1.f.

Combine

Multiply: (−1)^s × 1.f × 2^(e−1023). The exact decimal that the 64 bits truly represent.

The three formats at a glance

FormatTotalExponentMantissaBias~Decimal digits
half (float16)165 bits10 bits15~3–4
float (float32)328 bits23 bits127~7
double (float64)6411 bits52 bits1023~15–17

JavaScript numbers are always float64. The 32-bit mode in the visualizer is for contrast — it's what Math.fround and Float32Array use, and where GPU shaders and ML weights often live.

Watch 0.1 + 0.2 drift

Press reveal. Each row shows what JavaScript prints, the 17-digit truth, and the actual 64 bits — sign in gold, exponent in saffron, mantissa in cyan. Compare the last bits of 0.1 + 0.2 against 0.3: they differ by one.

0.1 + 0.2 · the 64-bit truth
Press reveal to expand the bits…

The nuances that senior devs know

This is where the 10,000 basic videos stop. Here's what actually bites in production.

Three roundings stack. 0.1 rounds slightly high. 0.2 rounds slightly high. Their exact sum doesn't land on a representable double either, so the result rounds too — and it rounds to the double one bit above the stored 0.3. That single ULP (unit in the last place) gap is the whole 0.00000000000000004. Every floating point operation can nudge the result, and across a loop the nudges accumulate.

(0.1 + 0.2) === 0.3 is false. Compare within a tolerance scaled to the magnitude of the operands — a fixed Number.EPSILON only works near 1.0:

compare.js
0.1 + 0.2 === 0.3;            // false
0.1 + 0.2;                  // 0.30000000000000004

// Compare within a tolerance instead of ===
const close = (a, b) =>
  Math.abs(a - b) <= Number.EPSILON * Math.max(Math.abs(a), Math.abs(b));

close(0.1 + 0.2, 0.3);     // true
Pro tip: the right epsilon depends on scale. Two numbers near a million are "equal" at a far coarser tolerance than two numbers near zero, because the gap between representable doubles grows with magnitude.

A balance in a float will drift. Accumulate thousands of + 0.01 operations and the rounding error becomes a visible discrepancy on a statement. The fix is to never let currency live in a float at all — store the smallest unit as an integer:

money.js
// Store paise, not rupees — integers never drift.
const price   = 1999;              // ₹19.99 as paise
const total   = price * 3;           // 5997 paise, exact
const display = (total / 100).toFixed(2);  // "59.97"

Integers are exact up to Number.MAX_SAFE_INTEGER (2⁵³ − 1); beyond that, reach for BigInt.

The exponent field reserves its extremes. When every exponent bit is zero, the implied leading 1 disappears — these are subnormals, filling the gap between the smallest normal number and zero. When every exponent bit is one, a zero mantissa means ±Infinity and a non-zero mantissa means NaN. That's why NaN !== NaN: many bit patterns are NaN, and the standard refuses to call any of them equal.

IEEE-754 is the substrate under almost every number you compute with:

DomainHow floats bite
Currency / financeDrift in accumulated sums — the canonical reason to use integer minor units or decimals.
Graphics / GPU32-bit floats everywhere; precision loss shows as z-fighting and jittery far-away geometry.
Machine learningfloat32/float16/bfloat16 weights; quantization trades mantissa bits for speed and memory.
GeospatialLarge coordinates lose precision in the mantissa — metres of error far from the origin.

The Karma of rounding

The Karma Connection

Karma is the principle that every action carries a consequence, deferred to the right time. In floating point the action is rounding — each operation that cannot be represented exactly leaves a tiny residue, an unpaid debt of precision. The debt is invisible at first; it comes due only when the errors accumulate across a loop, or when you compare with ===.

maya.js
// The name you see…
(0.1).toString();       // "0.1"   ← the rendered illusion (maya)

// …is not the value that is.
(0.1).toPrecision(17);  // "0.10000000000000001"  ← the stored truth

The name the senses accept ("0.1") is not the value that is (0.10000000000000001). To mistake the rendered form for the truth is to be bound by maya; to compare within an epsilon — to allow for the residue every action leaves — is to act with awareness of consequence. The float holds no grudge and rounds without malice; it simply keeps the nearest truth it can, and lets the rest go.

"Do not cling to the exactness of the form. Account for the residue, and the arithmetic stays honest." — the Gita, interpreted for numerical analysts 🙏

Floating Point Trivia

Three questions. No cheating. Your karma is watching.

Question 1
Why is (0.1 + 0.2) === 0.3 false in JavaScript?
Question 2
In a 64-bit double, how many bits are the exponent and what is its bias?
Question 3 — 🌶️ Spicy
Every exponent bit is 1 and the mantissa is all zeros. What value is this?

The Six Truths of Floating Point

Sign is Direction
One bit, no magnitude. 0 positive, 1 negative — flip it to negate without touching a single other bit.
Exponent is Scale
11 bias-encoded bits. Subtract 1023 for the true power of two; this is how one format spans the tiny and the vast.
Mantissa is Precision
52 bits plus an implied leading 1 — about 15–17 decimal digits. Where the infinite gets cut off and rounded.
Gaps Grow with Magnitude
Doubles aren't evenly spaced. Near zero they're dense; near a billion the gap between neighbours is itself large.
Never === Computed Floats
Compare within an epsilon. For money, use integer minor units — never let currency live in a float.
Maya — See Past the Form
The printed decimal is appearance; the bits are the truth. Account for the residue and the arithmetic stays honest.

कर्म करो, फल की चिंता मत करो