JavaScript Numbers
Unlike many languages, JavaScript has just one primitive numeric type for ordinary math: number. Whether you write 42, -7, or 3.14159, it’s all the same underlying type, stored the same way in memory. Understanding how that storage works — and where it breaks down — is essential, because it explains some of JavaScript’s most notorious “gotchas” like 0.1 + 0.2 not equaling 0.3.
Overview: How Numbers Work in JavaScript
Every JavaScript number is stored as a 64-bit IEEE-754 double-precision floating-point value. This single format is used for both integers and decimals — there is no separate int or float type. Internally, those 64 bits are split into three parts: 1 bit for the sign, 11 bits for the exponent, and 52 bits for the mantissa (the significant digits). That gives JavaScript about 15–17 significant decimal digits of precision.
Because the format is binary, it can represent powers of two exactly but cannot always represent decimal fractions exactly — the same way 1/3 can’t be written exactly in base 10. This is why 0.1 + 0.2 produces a tiny rounding error instead of a clean 0.3. It’s not a JavaScript bug; it’s how virtually every mainstream language that uses IEEE-754 floats (Python, Java, C, Go) behaves.
Integers are special-cased: JavaScript can represent whole numbers exactly up to Number.MAX_SAFE_INTEGER (253 − 1, or 9007199254740991). Beyond that, the 52-bit mantissa runs out of room, and some integers can no longer be distinguished from their neighbors. For numbers larger than that, JavaScript offers a second, separate numeric type: BigInt, written with a trailing n (e.g. 10n), which stores arbitrarily large integers exactly but cannot be mixed with regular numbers in the same expression.
There’s also a related but distinct Number object — a wrapper you rarely construct directly (avoid new Number(5)). When you call a method like (5).toFixed(2) on a primitive number, JavaScript briefly “boxes” the primitive into a temporary Number object, runs the method, and discards the wrapper. You get the convenience of methods without paying for object overhead in everyday use.
Syntax
Number literals can be written in several forms:
| Form | Example | Description |
|---|---|---|
| Decimal integer | 255 |
Base-10 whole number |
| Decimal float | 3.14 |
Contains a fractional part |
| Exponential | 2.5e3 |
2.5 × 103 = 2500 |
| Hexadecimal | 0xff |
Base-16, prefixed with 0x |
| Binary | 0b11111111 |
Base-2, prefixed with 0b |
| Octal | 0o377 |
Base-8, prefixed with 0o |
| Numeric separator | 1_000_000 |
Underscores for readability (ES2021+), ignored by the engine |
| BigInt | 9007199254740993n |
Arbitrary-precision integer, trailing n |
All of the non-BigInt forms above still produce an ordinary number value — the prefix only affects how the literal is parsed, not how it’s stored.
const decimal = 255;
const hex = 0xff;
const binary = 0b11111111;
const octal = 0o377;
const bigNumber = 1_000_000;
const exponent = 2.5e3;
console.log(decimal, hex, binary, octal, bigNumber, exponent);
Output:
255 255 255 255 1000000 2500
Notice that 0xff, 0b11111111, and 0o377 all evaluate to the same number, 255 — they’re just three different ways of writing the same integer.
Examples
Example 1: Basic arithmetic and type checks
const price = 19.99;
const quantity = 3;
const total = price * quantity;
console.log(total.toFixed(2));
console.log(typeof total);
console.log(Number.isInteger(quantity));
console.log(Number.isInteger(total));
Output:
59.97
number
true
false
Here toFixed(2) rounds and formats the result as a string with exactly two decimal places, which is the standard way to display currency-like values without worrying about tiny floating-point rounding noise. typeof always reports "number" for both integers and decimals, and Number.isInteger() checks whether a value has no fractional component.
Example 2: Converting and parsing strings into numbers
console.log(Number("42"));
console.log(Number("42px"));
console.log(parseInt("42px"));
console.log(parseFloat("3.14meters"));
console.log(Number(" 7 "));
console.log(Number(""));
console.log(Number(null));
console.log(Number(undefined));
Output:
42
NaN
42
3.14
7
0
0
NaN
Number() is strict: the entire string must be a valid number (ignoring surrounding whitespace), or the result is NaN. parseInt() and parseFloat() are more forgiving — they parse as much of the string as looks like a number from the left and stop at the first character that doesn’t fit. Note the quirky special cases: an empty string converts to 0, and so does null, while undefined becomes NaN.
Example 3: Precision limits, safe integers, and BigInt
console.log(0.1 + 0.2);
console.log(0.1 + 0.2 === 0.3);
console.log(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON);
console.log(Number.MAX_SAFE_INTEGER);
console.log(Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2);
const big = 9007199254740993n;
console.log(big);
console.log(typeof big);
Output:
0.30000000000000004
false
true
9007199254740991
true
9007199254740993n
bigint
This example shows the format’s limits in action. 0.1 + 0.2 doesn’t equal 0.3 exactly because none of those three values can be represented exactly in binary. Number.EPSILON (the smallest meaningful difference between two representable numbers near 1) gives a reliable tolerance for comparing floats. And once you go past Number.MAX_SAFE_INTEGER, regular numbers start colliding — MAX_SAFE_INTEGER + 1 and MAX_SAFE_INTEGER + 2 both round to the same stored value. BigInt sidesteps this entirely by using a completely different, arbitrary-precision internal representation.
Under the Hood: Why Floating-Point Math Looks “Wrong”
Think of a double as a sign, an exponent, and a 52-bit fraction, similar to scientific notation in binary: value = sign × mantissa × 2^exponent. Just as 1/3 becomes an infinitely repeating 0.333... in base 10, many ordinary decimal fractions like 0.1 become infinitely repeating binary fractions. Since only 52 bits are available for the mantissa, the engine must round the true value to the nearest representable double. Arithmetic then compounds these tiny rounding errors, which is exactly why 0.1 + 0.2 ends up as 0.30000000000000004 instead of a clean 0.3.
Integers behave better because whole numbers up to 253 − 1 can be represented exactly — there’s no repeating fraction to round. That’s the origin of Number.MAX_SAFE_INTEGER: past that boundary, the gaps between representable numbers grow larger than 1, so some integers become literally indistinguishable from their neighbors. This is a hardware-level constraint of IEEE-754, not something the JavaScript engine can “fix” without changing the whole numeric type — which is precisely why BigInt exists as a separate type with its own arithmetic operators and rules.
Common Mistakes
Mistake 1: Comparing floating-point numbers with strict equality
Because of rounding error, direct equality checks on computed decimals are unreliable.
const a = 0.1 + 0.2;
const b = 0.3;
console.log(a === b);
function areClose(x, y, tolerance = Number.EPSILON) {
return Math.abs(x - y) < tolerance;
}
console.log(areClose(a, b));
Output:
false
true
Instead of a === b, compare the absolute difference against a small tolerance (as shown), or round both values to a fixed number of decimal places with toFixed() before comparing.
Mistake 2: Trusting === NaN to detect “not a number”
const result = Number("abc");
console.log(result === NaN);
console.log(Number.isNaN(result));
console.log(isNaN("hello"));
Output:
false
true
true
NaN is the only JavaScript value that is never equal to itself, so x === NaN always returns false, even when x genuinely is NaN. Use Number.isNaN(x) instead, which checks the type strictly. The older global isNaN() first coerces its argument to a number, which can produce misleading results for non-numeric inputs like strings — prefer Number.isNaN() in modern code.
Mistake 3: Forgetting that number prefixes change how strings parse
console.log(parseInt("010"));
console.log(parseInt("010", 10));
console.log(parseInt("0x10"));
Output:
10
10
16
Modern engines no longer treat a leading zero as octal, so parseInt("010") correctly returns 10. But a 0x prefix is still auto-detected as hexadecimal, so parseInt("0x10") returns 16, not 10 — a common source of confusion when parsing user input or CSV data that might contain hex-looking strings. Always pass an explicit radix (usually 10) to parseInt() so the intent is unambiguous to both the engine and to anyone reading the code.
Best Practices
- Use
Number.isInteger()andNumber.isNaN()instead of the older globalisNaN()/relying on implicit coercion — they don’t silently convert their argument first. - Never compare floating-point results with
===; use a tolerance (Number.EPSILON) or round both sides first. - Always pass a radix to
parseInt(), e.g.parseInt(value, 10), even though it’s rarely strictly required today — it documents intent. - Use
toFixed()only for display formatting (it returns a string); keep doing arithmetic on the original numeric value. - For money or anything requiring exact decimal precision, avoid raw floating-point math entirely — work in integer cents, or use a dedicated decimal library.
- Reach for
BigIntonly when you truly need integers beyondNumber.MAX_SAFE_INTEGER(e.g. large IDs, cryptography); it cannot be mixed with regular numbers in one expression and is slower for typical arithmetic. - Prefer numeric separators (
1_000_000) for large literals in source code — they improve readability with zero runtime cost.
Practice Exercises
- Exercise 1: Write a function
roundToCents(amount)that takes a floating-point dollar amount and returns it rounded to exactly two decimal places as a number (not a string). Test it with an amount like19.005. - Exercise 2: Write a function
isSafeInteger(value)that returnstrueonly ifvalueis a whole number within the safe integer range, andfalseotherwise. Test it againstNumber.MAX_SAFE_INTEGERandNumber.MAX_SAFE_INTEGER + 10. - Exercise 3: Given the array of strings
["12", "7.5abc", "0x1A", "", " 3 "], useNumber()orparseFloat()as appropriate to convert each into a proper number, and predict the output before you check it.
Summary
- JavaScript has a single
numbertype: a 64-bit IEEE-754 double, used for both integers and decimals. - Literals can be written in decimal, hex (
0x), binary (0b), octal (0o), or exponential form, and can use_separators for readability. - Because decimals are stored in binary, many fractions can’t be represented exactly, causing results like
0.1 + 0.2 !== 0.3. - Integers are exact only up to
Number.MAX_SAFE_INTEGER(253 − 1); beyond that, useBigIntfor exact arithmetic. Number(),parseInt(), andparseFloat()convert strings to numbers with different strictness and stopping rules.- Always use
Number.isNaN()over=== NaN, and compare floats with a tolerance, not strict equality.
