skip to content

Numbers and Numeric Types

Until BigInt arrived, every JavaScript number was a 64-bit float — which explains rounding errors, integer limits, and a family of special values that break normal comparison. Interviewers reach for this group whenever money, IDs, or precise comparisons are involved.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

explore

questions

15

In JavaScript, why does `0.1 + 0.2 === 0.3` evaluate to false, and what does `0.1 + 0.2` actually produce?

level: juniorimportance: must knowfreq 88%

answer

  1. one numeric type, binary double
  2. one tenth repeats in base two
  3. literals rounded before arithmetic runs
  4. sum lands one step above 0.3
  5. 0.30000000000000004

basics

~10 s

JavaScript numbers are IEEE-754 binary doubles, and 0.1, 0.2 and 0.3 cannot be written exactly in binary. The stored approximations add up to 0.30000000000000004, a different double from 0.3, so === is false.

solid answer

~50 s

Every JavaScript `Number` is an IEEE-754 double-precision binary float: a sign, an 11-bit exponent and 53 significant bits. One tenth in binary is a repeating fraction, the same way one third is repeating in decimal, so the literals `0.1` and `0.2` are each rounded to the nearest representable double before any arithmetic happens. Adding those two approximations gives an exact sum that is itself not representable, so it is rounded again — and it lands one representable step above the double you get from writing `0.3`. The result prints as `0.30000000000000004`, and `0.1 + 0.2 === 0.3` is false. This is not a JavaScript defect; it is how binary floating point behaves in any language that uses it. The practical response is to compare with a tolerance, or to avoid floats entirely for exact domains like money by working in integer minor units.

go deeper

for a junior

Be able to say plainly that numbers are binary doubles, that 0.1 and 0.2 have no exact binary form, and that the sum is 0.30000000000000004. Do not call it a JavaScript bug.

for a middle

Explain the mechanics: 53 significant bits, rounding at parse time and again after the operation, and shortest-round-trip printing that hides the error until it cannot. Name a case that is exact, such as 0.5 + 0.25.

for a senior

Show where this actually bites in production — price times 100 landing below the whole cent, thresholds compared with ===, totals that fail reconciliation — and describe the representation change that removes the class of bug rather than patching one site.

for a principal

Own the policy: decide which domains in the system must be exact and which tolerate bounded error, fix the representation at the boundaries so exactness is not re-litigated per feature, and make rounding a stated, tested rule rather than an incidental toFixed call.

## Every JavaScript number is a double JavaScript has one built-in numeric type for ordinary arithmetic: `Number`, which the specification defines as an IEEE-754 double-precision binary floating-point value. A double is 64 bits: 1 sign bit, 11 exponent bits, and 52 stored fraction bits that give 53 bits of significand (the leading bit is implicit). There is no separate integer type in play here — `1`, `1.5` and `1e300` are all doubles. That format can represent a number only if it can be written as *m × 2^e*, where *m* fits in 53 bits and *e* is in range. Every value you write in source code is rounded to the nearest such number at parse time. ## Why one tenth is not representable In decimal, 1/3 has no finite representation: 0.333… repeats forever. Which fractions terminate depends on the base: a fraction terminates in base *b* only when its denominator's prime factors all divide *b*. Base ten has factors 2 and 5, so 1/2, 1/4, 1/5 and 1/10 all terminate in decimal. Base two has only the factor 2, so only denominators that are powers of two terminate. 1/10 has a factor of 5 in the denominator, so in binary it is the repeating fraction 0.0001100110011… and must be cut off at 53 bits. You can see the stored value by asking for more decimal digits than the shortest printout gives: ```js (0.1).toFixed(20); // "0.10000000000000000555" (0.2).toFixed(20); // "0.20000000000000001110" (0.3).toFixed(20); // "0.29999999999999998890" ``` None of the three is the decimal number you typed. `0.1` is stored slightly high, `0.3` slightly low. ## Three roundings, not one The error people blame on `+` actually accumulates in three steps: 1. `0.1` rounds to the nearest double (slightly above one tenth). 2. `0.2` rounds to the nearest double (slightly above two tenths). 3. The exact mathematical sum of those two doubles is computed, then rounded to the nearest double. ```js (0.1 + 0.2).toFixed(20); // "0.30000000000000004441" ``` That result is one representable step above the double stored for the literal `0.3`, so the two are genuinely different values and `===` correctly reports false. Strict equality is not being clever or lenient here; it is comparing two distinct doubles. The same effect shows up wherever binary rounding is involved, not just in addition: ```js 0.1 * 3; // 0.30000000000000004 0.07 * 100; // 7.000000000000001 4.35 * 100; // 434.99999999999994 ``` The last one is the classic money bug: a price of 4.35 multiplied by 100 to get "cents" is below 435, so truncating gives 434. ## Why printing usually looks clean If `0.1` is really 0.1000000000000000055…, why does `console.log(0.1)` print `0.1`? Because JavaScript's number-to-string conversion prints the *shortest decimal string that round-trips* — the fewest digits that, when parsed back, yield the same double. For the double nearest one tenth, that string is `"0.1"`. For the sum above, no short string round-trips, so you get the full `0.30000000000000004`. The ugly digits are not noise added by printing; they are the moment the approximation stops being hideable. ## What is exact Plenty of arithmetic is exact. Fractions whose denominators are powers of two are represented perfectly, so `0.5 + 0.25 === 0.75` is true, and `0.125` is stored exactly. Whole numbers are also exact as long as they stay within the range where 53 bits can hold every integer, which is why counting, indexing and array lengths never surprise anyone. The result is also fully deterministic. IEEE-754 specifies round-to-nearest-even for these operations and the language specification requires it, so `0.1 + 0.2` produces the identical double in every conforming engine, on every platform. This is a portable, predictable approximation, not a random one. ## What to do instead For comparisons of computed values, test that the difference is small relative to the magnitudes involved rather than testing for exact equality. For exact decimal domains — currency above all — do not store the amount as a fractional double at all: keep it in integer minor units (cents) or as a decimal string, do the arithmetic in integers, and convert to a decimal presentation only at the edges. And do not reach for `toFixed` as a repair: it formats a double that is already the wrong value, so it cannot recover precision that was lost when the literal was parsed.

  • Which decimal literals are stored exactly as doubles, and can you name one?
    Any fraction whose denominator is a power of two, within the exponent range — 0.5, 0.25, 0.125, 0.75. So `0.5 + 0.25 === 0.75` is true and `(0.125).toFixed(20)` shows a clean value. Denominators with a factor of 5, like tenths and hundredths, always repeat in binary and must be rounded.
  • Does `0.1 + 0.2` give the same result in every JavaScript engine and on every platform?
    Yes. The language specification pins `Number` to IEEE-754 double precision with round-to-nearest-even, so the operation is fully determined. Every conforming engine on every CPU produces the identical double, `0.30000000000000004`. The imprecision is portable and reproducible, which is exactly why you can write reliable tolerance checks against it.
  • If `0.1` is really slightly larger than one tenth, why does printing it show `0.1`?
    Number-to-string conversion emits the shortest decimal string that parses back to the same double. For the double nearest one tenth that string is `"0.1"`. Ask for more digits with `(0.1).toFixed(20)` and the stored value `0.10000000000000000555` appears. Printing hides the approximation; it does not remove it.

saying these in an interview costs you the question

  • Says JavaScript's addition operator is buggy
  • Claims JavaScript uses 32-bit floats so precision is low
  • Thinks the literals are exact and only + rounds
  • Believes toFixed(2) makes the arithmetic exact again
  • Says the result differs between engines or machines

context

open as a page

In JavaScript, why does NaN === NaN evaluate to false, and how do you reliably test whether a value is NaN?

level: juniorimportance: must knowfreq 72%

basics

~10 s

NaN is the only JavaScript value not equal to itself: IEEE-754 makes every comparison involving NaN false, so === and indexOf never find it. Test with Number.isNaN(x), which checks the value without converting it.

open as a page

How should you compare two computed JavaScript numbers for approximate equality, and what does `Number.EPSILON` actually represent?

level: middleimportance: must knowfreq 58%

basics

~20 s

Number.EPSILON is the gap between 1 and the next representable double, about 2.22e-16 — a relative spacing near 1, not a universal error bound. Compare with a tolerance scaled to the operands' magnitude, not a fixed epsilon.

open as a page

In JavaScript, why does the expression 9007199254740993 === 9007199254740992 evaluate to true, and what does Number.MAX_SAFE_INTEGER have to do with it?

level: middleimportance: must knowfreq 65%

basics

~20 s

A JavaScript number carries 53 bits of significand, so integers above 2^53 can no longer all be represented; the literal 9007199254740993 rounds to 9007199254740992 at parse time. Number.MAX_SAFE_INTEGER, 2^53 - 1, marks the last integer whose successor is still distinct.

open as a page

How do JavaScript's global isNaN() and isFinite() differ from Number.isNaN() and Number.isFinite(), and which would you use to validate a numeric field?

level: middleimportance: must knowfreq 58%

basics

~10 s

The global isNaN and isFinite convert their argument to a number first, so isNaN("") is false and isFinite("100") is true. The ES2015 Number.isNaN and Number.isFinite never convert: any non-number returns false.

open as a page

In JavaScript, what does Number.isSafeInteger(value) check that Number.isInteger(value) does not, and what do both do with a non-number argument?

level: juniorimportance: should knowfreq 40%

basics

~20 s

Number.isInteger asks only whether the value is a whole number; Number.isSafeInteger additionally requires it to lie within plus or minus 2^53 - 1, the range where every integer is uniquely representable. Both return false for any non-number, with no coercion.

open as a page

In JavaScript, `(1.005).toFixed(2)` returns "1.00" rather than "1.01". Why, and what else should you know about `Number.prototype.toFixed`?

level: middleimportance: should knowfreq 46%

basics

~20 s

The double stored for the literal 1.005 is 1.00499999999999989, just under the halfway point, so toFixed rounds it down. toFixed rounds the stored binary value, not the decimal you typed, and it returns a string.

open as a page

In JavaScript, why does 1n + 1 throw a TypeError while 1n == 1 and 1n < 2 are both true?

level: middleimportance: should knowfreq 50%

basics

~20 s

Arithmetic operators refuse mixed BigInt and number operands, because implicitly converting either way could silently lose precision — the exact problem BigInt exists to prevent. Comparison operators are exempt: they compare mathematical values, so mixed comparisons are safe and allowed.

open as a page

In JavaScript, which operations produce Infinity and which produce NaN? Explain the rules with examples.

level: middleimportance: should knowfreq 42%

basics

~10 s

A nonzero number divided by zero, or arithmetic that overflows the double range, gives a signed Infinity. Genuinely indeterminate forms give NaN: 0/0, Infinity - Infinity, Infinity * 0, Infinity / Infinity. Neither throws.

open as a page

How does a value in JavaScript end up as -0, and how do you detect it given that -0 === 0 is true?

level: middleimportance: should knowfreq 38%

basics

~20 s

IEEE-754 keeps a sign bit even when the magnitude is zero, so 0 * -5, Math.round(-0.4) and underflow all give -0. Strict equality cannot see it; use Object.is(x, -0) or check that 1 / x is -Infinity.

open as a page

How would you represent and compute monetary amounts in a JavaScript service so that totals do not drift, and where should the conversions between representations happen?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Never hold money in a fractional double. Keep amounts as integers in the smallest unit — cents — or as decimal strings, do all arithmetic on the integers, apply one explicit rounding rule where division forces one, and convert to decimal only at the system's edges.

open as a page

A JavaScript client reads records from a JSON API whose primary keys are 64-bit integers, and lookups by those keys intermittently miss while two distinct records sometimes collapse into one. How do you confirm the cause and fix it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

JSON.parse maps every JSON number to a double, so an identifier above 2^53 arrives already rounded and a reviver cannot recover it. Confirm by comparing the raw response text with the parsed value, then have the producer emit such identifiers as JSON strings.

open as a page

A JavaScript dashboard computes a total from API data and intermittently renders it as NaN. How do you find where the NaN came from, and how do you stop this class of bug?

level: seniorimportance: should knowfreq 36%

basics

~20 s

NaN is contagious and carries no provenance: one bad field poisons every downstream result. Bisect by testing each input with Number.isFinite instead of inspecting the total, then validate and reject non-finite values at the boundary where data enters.

open as a page

A JavaScript reporting job sums millions of floating-point amounts, and its totals disagree with the source ledger by small amounts that change when the rows are processed in a different order. How do you diagnose this and decide on a fix?

level: principalimportance: nice to knowfreq 24%

basics

~20 s

Floating-point addition is not associative: each partial sum is rounded, so a different order gives a different total, and large running sums absorb small addends entirely. Diagnose by comparing against an exact integer sum, then decide whether the domain needs exactness or bounded error.

open as a page

For a JavaScript codebase handling values that exceed the safe integer range, how do you decide between representing them as BigInt, as opaque strings, or keeping them as numbers, and what does each choice cost across the system?

level: principalimportance: nice to knowfreq 25%

basics

~20 s

The deciding question is whether you do arithmetic on the value. Opaque strings suit identifiers you only compare and echo; BigInt suits genuine exact arithmetic and costs you JSON serialization, Math support and mixing with numbers; plain numbers stay only where values provably stay under 2^53.

open as a page