JavaScript has one number type, not two. There is no separate integer and float type, the arithmetic runs on IEEE 754 double-precision values, and that single decision explains the rounding surprises, the safe-integer ceiling and the need for BigInt. Understanding the numbers removes most of the "why is this wrong?" moments in everyday JavaScript.
Does JavaScript have separate integer and float types?
No. Every number you write — 42, 3.14, 1e21 — becomes the same primitive type, number, stored as a 64-bit floating-point value. That means one number type covers the whole range, which is convenient, but it also means integers are only exact up to a limit, and that is a limit you can reach sooner than you might expect.
console.log(typeof 42); // "number"
console.log(typeof 3.14); // "number"
console.log(typeof 1e21); // "number"
console.log(Number.isInteger(42)); // true
console.log(Number.isInteger(3.0)); // true — 3.0 is just 3
console.log(Number.isInteger(3.5)); // false
The line worth pausing on is 3.0. There is no way to tell an integer from a float in JavaScript, so Number.isInteger asks a different question: does this value have no fractional part? 3.0 qualifies because it is exactly 3.
Why is 0.1 + 0.2 not exactly 0.3?
Because 0.1 and 0.2 cannot be represented exactly in binary floating point. Their stored values are extremely close to the decimal versions, and adding those approximations leaves a remainder of roughly 5.5e-17 — invisible on paper, visible in a comparison.
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
console.log(0.1 + 0.2 - 0.3); // 5.551115123125783e-17
console.log(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON); // true
Number.EPSILON is the gap between 1 and the next representable double, and it doubles as the tolerance to use when comparing numbers that were produced by arithmetic. That last line prints true.
Two more traps live in the same family. Adding a floating-point number and an integer usually gives an ugly result: 0.1 + 0.2 + 1 prints 1.3000000000000003. And division always produces a float, so 7 / 2 is 3.5, not 3. If you want integer division, use Math.trunc or Math.floor and say so in the variable name.
What is the largest integer JavaScript can count exactly?
Number.MAX_SAFE_INTEGER is 9007199254740991 — two to the fifty-third power, minus one. Below that, every integer has a unique double representation. Above it, adjacent integers start collapsing onto the same stored value, which means a counter can silently skip a number.
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.MAX_SAFE_INTEGER + 1); // 9007199254740992
console.log(Number.MAX_SAFE_INTEGER + 2); // 9007199254740992 ← 2 vanished
console.log(Number.isSafeInteger(9007199254740991)); // true
console.log(Number.isSafeInteger(9007199254740992)); // false
Look at the third and fourth lines. Adding 1 gives an exact result, but adding 2 gives the same answer as adding 1 — the value 9007199254740993 is not representable, so it rounds down to ...992. The number did not overflow into an error; it quietly changed, which is the dangerous part. Number.isSafeInteger is the check to run when a value might have come from outside your control, such as a database identifier.
How do I convert a string into a number?
There are three ways, and they behave differently on messy input.
console.log(Number("42")); // 42
console.log(Number("42px")); // NaN ← the whole string must be numeric
console.log(Number("")); // 0 ← the empty string becomes zero
console.log(parseInt("42px")); // 42 ← stops at the first bad character
console.log(parseInt("0x1f")); // 31 ← hex is auto-detected
console.log(parseInt("1f", 16)); // 31 ← explicit radix
console.log(parseFloat("3.5kg")); // 3.5
Number() is strict: the entire string has to be a valid number or you get NaN. parseInt is lenient: it reads the leading digits and throws the rest away. Both behaviours are useful in different places — Number() when you want validation, parseInt when you are extracting a number from the start of a string.
Two rules make parsing safe. Always pass the radix to parseInt: parseInt(value, 10) for ordinary numbers, or parseInt(value, 16) for hex, so the behaviour never depends on what the string happens to start with. And always check the result with Number.isNaN, because NaN is the only value in JavaScript that is not equal to itself — NaN === NaN is false.
When do I need BigInt?
When the number genuinely has to be exact and genuinely exceeds the safe range: identifiers, cryptographic values, nanosecond timestamps, big monetary amounts in the smallest unit. BigInt is a separate primitive type, and you write the literal with a trailing n.
const big = 9007199254740993n;
console.log(typeof big); // "bigint"
console.log(big); // 9007199254740993n
console.log(big + 1n); // 9007199254740994n
console.log(typeof 9007199254740993); // "number" — already imprecise!
try {
console.log(big + 1); // mixing the two types is an error
} catch (error) {
console.log("mixed:", error.constructor.name, "-", error.message);
}
The fourth line is the one to remember: the plain number literal has already lost a digit before you ever touched BigInt. The last line shows that BigInt will not silently coerce — mixing a BigInt with a Number throws a TypeError with the message Cannot mix BigInt and other types, use explicit conversions, so the mistake surfaces at the point it happens rather than as a wrong number later.
BigInt is not a drop-in replacement for number. It has no toFixed, it will not divide, and JSON cannot represent it — JSON.stringify throws on a BigInt. Use it for the specific values that need exactness and keep ordinary arithmetic on number.
How do I format a number for display?
Numbers print with as many digits as they need and no separators, which is rarely what you want on screen.
const amount = 24950.5;
console.log(amount.toFixed(2)); // "24950.50"
console.log(amount.toFixed(0)); // "24951"
console.log(amount.toPrecision(6)); // "24950.5"
console.log((1234567.891).toLocaleString("en-IN")); // "12,34,567.891"
console.log(Math.round(amount)); // 25051
toFixed gives you a fixed number of decimal places and returns a string, not a number — remember that when you use it in arithmetic. toPrecision sets the total number of significant digits, which suits scientific or engineering values. toLocaleString is the one to reach for on a real page: it applies the grouping and decimal conventions of a locale, so the same number reads correctly for different audiences.
For real money, the right tool is Intl.NumberFormat, which is the engine behind toLocaleString but far more configurable — you can fix the currency, force a minimum number of decimals and control where the separators go.
What to remember about JavaScript numbers
- There is one number type. Integers and floats are the same thing, and
Number.isIntegerasks whether a value has a fractional part, not what type it was written as. - Floating point means
0.1 + 0.2 !== 0.3. Compare with a tolerance, not with equality. Number.MAX_SAFE_INTEGERis 9007199254740991. Past that, integers start collapsing silently — check withNumber.isSafeInteger.Number()parses strictly,parseIntparses leniently. Always giveparseInta radix, and always check forNaNwithNumber.isNaN.BigIntis exact past the safe range, but it does not mix withnumberand cannot be serialised to JSON.- Use
toFixedfor fixed decimals,toPrecisionfor significant digits andIntl.NumberFormatfor anything user-facing.
Try it yourself: write a script that takes three prices as strings such as "19.99", "1200" and "7.50", converts them with Number(), adds them, and prints the total with toFixed(2). Now repeat it with parseFloat and with parseInt, and notice which total is right. Then add a fourth price of "abc" and see the difference between a strict NaN and a silent zero — that difference is exactly the bug you want to avoid in a checkout flow.