Numbers in JavaScript
Learn how JavaScript stores all numbers as 64-bit floats, why 0.1 + 0.2 !== 0.3, how to fix floating point issues, NaN, Infinity, Number methods, Math object, and BigInt.
Let me be upfront with you: numbers in JavaScript look simple on the surface. You type 5, you type 3.14, JavaScript handles it. No int, no double, no float types to declare like in some other languages. Easy, right?
Until you run 0.1 + 0.2 in your browser console and JavaScript looks you dead in the eye and says 0.30000000000000004.
That moment breaks a lot of beginners. And honestly, it trips up experienced developers too, because it’s not a JavaScript bug. It’s how computers store decimal numbers at the hardware level, and JavaScript just makes it very visible.
In this article we’re going to go through everything about numbers in JavaScript: how they’re stored, why floating point math behaves the way it does, NaN and Infinity, all the useful Number methods, the Math object, and BigInt for when your numbers get astronomically large. This is part of the ongoing JavaScript Data Types series, and numbers deserve their own full article.
Let’s get into it.
01JavaScript Has Only One Number Type (And That’s On Purpose)
Here’s the first thing to lock in: JavaScript does not have separate types for integers and floating-point numbers. Unlike C, Java, or Python, there’s no int, no long, no double. There’s just number.
Every single number in JavaScript, whether that’s 1, 1000, 3.14, or -0.000001, is stored as a 64-bit double-precision floating-point number. This format follows the IEEE 754 standard, the same standard used by most modern programming languages for decimal math.
typeof 42 // "number"
typeof 3.14 // "number"
typeof -7 // "number"
typeof 0 // "number"
typeof 1e6 // "number" (scientific notation: 1,000,000)
They all come back as "number". Same type, always.
What Does 64-bit Double-Precision Actually Mean?
This sounds like hardware jargon, but let me break it down in a way that actually makes sense. That 64 bits is split into three sections:
Sign (1 bit) : positive or negative
Exponent (11 bits) : scale of the number
Mantissa (52 bits) : the actual digits
Those 52 bits for the mantissa are where your actual number digits live. And here’s the catch: 52 bits can only represent a finite number of decimal values. Most decimal fractions (like 0.1) simply cannot be stored exactly in binary. The computer stores the closest possible approximation.
For integers, this gives you an exact safe range up to 2^53 - 1, which is 9007199254740991. JavaScript even stores this as a constant:
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.MIN_SAFE_INTEGER); // -9007199254740991
Within that range, integer math is exact and reliable. Decimal math is where things get interesting.
02The JavaScript Floating Point Problem Explained
Okay, this is the part everyone talks about. Open your browser console right now and type this:
console.log(0.1 + 0.2); // 0.30000000000000004
Why does this happen? Not because JavaScript is broken. Not because the engineers at Netscape messed up in 1995. It happens because 0.1 and 0.2 cannot be represented exactly in binary.
Think of it like this: in decimal, you can’t write 1/3 as a finite decimal. It becomes 0.3333… forever. In binary, 0.1 has the same problem. Its binary representation is an infinitely repeating pattern. So the computer stores a rounded-off version. When you add two rounded-off versions, you get a tiny error in the result.
This is not just a JavaScript thing. Try it in Python, PHP, Ruby, Java, you’ll see the same behavior. They all use IEEE 754 under the hood.
Explore the Floating Point Problem Yourself
Play around with this demo. Try adding 0.1 + 0.2, then 0.3 + 0.6, then something like 1 + 2. You’ll quickly see which additions suffer from precision loss and which ones don’t:
How to Fix Floating Point Precision Issues in JavaScript
There are a few practical ways to handle this. The right approach depends on what you need:
Option 1: toFixed() for display
If you just need to show a result to the user (a price, a percentage), round it at display time:
let result = 0.1 + 0.2;
console.log(result.toFixed(2)); // "0.30"
console.log(result.toFixed(1)); // "0.3"
Note that toFixed() returns a string, not a number. Keep that in mind.
Option 2: Multiply, calculate, then divide
For financial or precise math, work with integers underneath. Multiply your numbers to remove the decimal, do the math, then divide back:
// Adding $0.10 + $0.20 in cents
let a = 0.1;
let b = 0.2;
// Work in cents (integers)
let result = (Math.round(a * 100) + Math.round(b * 100)) / 100;
console.log(result); // 0.3 (exact)
Option 3: Number.EPSILON for comparisons
Never compare floating-point numbers with === directly. Use Number.EPSILON which represents the smallest difference between two representable numbers:
// The wrong way
console.log(0.1 + 0.2 === 0.3); // false
// The right way: check if the difference is "close enough"
function approximatelyEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
console.log(approximatelyEqual(0.1 + 0.2, 0.3)); // true
03NaN in JavaScript: The Number That Isn't a Number
Here's one of JavaScript's quirky things. NaN stands for "Not a Number," and it appears whenever a math operation doesn't produce a valid numeric result.
console.log("hello" * 2); // NaN
console.log(0 / 0); // NaN
console.log(Math.sqrt(-1)); // NaN
console.log(parseInt("abc")); // NaN
Here's the twist: typeof NaN is "number". Yes, really.
console.log(typeof NaN); // "number"
The reason is that NaN lives in the number type space, it's just a special value within it that signals a failed numeric operation. Think of it as "the result was supposed to be a number, but the math went wrong."
The NaN Quirk You Need to Know
This one surprises basically everyone: NaN is not equal to itself.
console.log(NaN === NaN); // false
console.log(NaN == NaN); // false
This comes directly from the IEEE 754 spec. It's intentional. So you can't check for NaN with ===. You need to use one of these two approaches:
// Option 1: Number.isNaN() - the recommended way
console.log(Number.isNaN(NaN)); // true
console.log(Number.isNaN("hello")); // false (it's a string, not NaN)
console.log(Number.isNaN(undefined)); // false
// Option 2: the legacy isNaN() - be careful with this one
console.log(isNaN(NaN)); // true
console.log(isNaN("hello")); // true <-- "hello" gets coerced, then it's NaN
console.log(isNaN(undefined)); // true <-- undefined coerces to NaN too
Number.isNaN(), not isNaN(). The global isNaN() coerces its argument to a number first, which means non-numeric values like "hello" or undefined also return true. Number.isNaN() is strict: it only returns true for the actual NaN value.
Where NaN Shows Up in Practice
That last one is important. NaN is contagious. Once a NaN gets into a calculation, the result stays NaN. If you suddenly see NaN appearing deep in your app, trace it backward through the operations.
04Infinity and -Infinity: When Numbers Go Off the Chart
JavaScript also has Infinity and -Infinity, which represent numbers that exceed the maximum representable value. Both are valid JavaScript values of type "number".
console.log(1 / 0); // Infinity
console.log(-1 / 0); // -Infinity
console.log(Infinity + 1); // Infinity
console.log(Infinity - Infinity); // NaN (indeterminate!)
console.log(typeof Infinity); // "number"
JavaScript also stores the maximum safe float as a constant:
console.log(Number.MAX_VALUE); // 1.7976931348623157e+308
console.log(Number.MIN_VALUE); // 5e-324 (smallest positive number, not most negative)
console.log(Number.POSITIVE_INFINITY); // Infinity
console.log(Number.NEGATIVE_INFINITY); // -Infinity
To check if a number is finite (i.e., not Infinity, not -Infinity, and not NaN), use Number.isFinite():
console.log(Number.isFinite(100)); // true
console.log(Number.isFinite(Infinity)); // false
console.log(Number.isFinite(NaN)); // false
console.log(Number.isFinite("50")); // false (it's a string, not a number)
isFinite() coerces values first. Number.isFinite() is the strict, reliable version. Use that one.
05JavaScript Number Methods: The Ones You'll Actually Use
The Number object comes with a solid set of methods for converting, parsing, and formatting numbers. These come up constantly in real projects. Let's go through all of them properly.
parseInt vs parseFloat: Which One to Use?
This comes up a lot when reading user input from a form or processing data from an API. Here's a clean comparison:
| Expression | parseInt | parseFloat | Number() |
|---|---|---|---|
| "42" | 42 | 42 | 42 |
| "3.99" | 3 (truncates) | 3.99 | 3.99 |
| "12px" | 12 (partial) | 12 (partial) | NaN |
| "px12" | NaN | NaN | NaN |
| "" | NaN | NaN | 0 |
| true | NaN | NaN | 1 |
| null | NaN | NaN | 0 |
My rule of thumb: use parseInt() when you're reading CSS values like "16px". Use parseFloat() when you need decimals from a string like "3.14rem". Use Number() when you want strict conversion with no partial parsing tricks.
And always pass a radix to parseInt():
// Always specify the base (radix) for parseInt
parseInt("010", 10); // 10 (decimal)
parseInt("010"); // Could be 10 or 8 depending on the engine (don't rely on it)
parseInt("0x1A", 16); // 26 (hexadecimal)
parseInt("1010", 2); // 10 (binary)
06The Math Object: Your Built-in Calculator
The Math object is not a class you instantiate. You don't do new Math(). It's a built-in global object with static methods and constants you call directly. It's one of the most useful things in JavaScript for numeric work.
// Math constants
console.log(Math.PI); // 3.141592653589793
console.log(Math.E); // 2.718281828459045
// Rounding
console.log(Math.round(4.5)); // 5
console.log(Math.round(4.4)); // 4
console.log(Math.ceil(4.1)); // 5 (always rounds up)
console.log(Math.floor(4.9)); // 4 (always rounds down)
console.log(Math.trunc(4.9)); // 4 (removes decimal, no rounding)
console.log(Math.trunc(-4.9)); // -4 (not -5, just removes decimal)
// Min/Max
console.log(Math.max(1, 5, 3, 2)); // 5
console.log(Math.min(1, 5, 3, 2)); // 1
console.log(Math.max(...[10, 20, 5])); // 20 (with spread operator)
// Power and roots
console.log(Math.pow(2, 10)); // 1024
console.log(2 ** 10); // 1024 (modern exponentiation operator)
console.log(Math.sqrt(144)); // 12
console.log(Math.cbrt(27)); // 3 (cube root)
// Absolute value
console.log(Math.abs(-15)); // 15
console.log(Math.abs(15)); // 15
// Random numbers
console.log(Math.random()); // random float: 0 (inclusive) to 1 (exclusive)
// Random integer between min and max (inclusive)
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
console.log(randomInt(1, 10)); // e.g. 7
console.log(randomInt(1, 100)); // e.g. 43
One thing worth knowing: Math.round() follows "round half up" rules. Math.round(0.5) gives 1, and Math.round(-0.5) gives 0 (not -1). This sometimes catches people off guard when dealing with negative numbers.
07BigInt in JavaScript: Numbers Beyond Safe Integer Range
Remember Number.MAX_SAFE_INTEGER? That's 9007199254740991. Go past that and integers stop being exact:
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.MAX_SAFE_INTEGER + 1); // 9007199254740992
console.log(Number.MAX_SAFE_INTEGER + 2); // 9007199254740992 (same! precision lost)
console.log(Number.MAX_SAFE_INTEGER + 3); // 9007199254740994
This matters in real situations: working with large database IDs, cryptographic numbers, financial calculations with very large values, or anything involving 64-bit integers from a backend API.
That's exactly why BigInt was added to JavaScript in ES2020. It's a separate numeric type that can handle integers of arbitrary size, accurately.
How to Create BigInt Values
// Two ways to create a BigInt:
const a = 9007199254740991n; // append "n" to a number literal
const b = BigInt("9007199254740991"); // or use the BigInt() function
console.log(a); // 9007199254740991n
console.log(typeof a); // "bigint"
// BigInt arithmetic stays exact
const big = 9007199254740991n;
console.log(big + 1n); // 9007199254740992n (exact!)
console.log(big + 2n); // 9007199254740993n (exact!)
console.log(big * 2n); // 18014398509481982n
BigInt Rules and Gotchas
BigInt is powerful but comes with a few rules you should know upfront:
// You CANNOT mix BigInt and regular number in the same operation
const a = 10n;
const b = 5;
// console.log(a + b); // TypeError: Cannot mix BigInt and other types
// Convert explicitly when needed
console.log(a + BigInt(b)); // 15n (convert b to BigInt first)
console.log(Number(a) + b); // 15 (convert a to Number first)
// BigInt division truncates (no decimals)
console.log(10n / 3n); // 3n (not 3.333...)
console.log(7n % 2n); // 1n (modulo works fine)
// Comparison with regular numbers works (uses type coercion)
console.log(10n == 10); // true (loose equality)
console.log(10n === 10); // false (strict equality, different types)
console.log(10n > 5); // true
console.log(10n < 20); // true
// BigInt doesn't work with Math methods
// Math.sqrt(9n); // TypeError
// Use ** for exponentiation
console.log(2n ** 64n);
// 18446744073709551616n (exact! This would be wrong with regular Number)
// JSON doesn't handle BigInt by default
// JSON.stringify(42n); // TypeError: Do not know how to serialize a BigInt
// You'll need a custom replacer function for that
number is fine. The float precision issues matter more day-to-day.
08Number Literals: All the Ways to Write Numbers in JavaScript
One thing that doesn't get talked about enough is that JavaScript supports several different ways to write numeric literals in your code. These are all valid:
// Standard decimal
let a = 255;
// Hexadecimal (base 16) - common in CSS colors, bitwise ops
let hex = 0xFF; // 255
let hex2 = 0x1A; // 26
// Binary (base 2) - useful with bitwise operations
let bin = 0b11111111; // 255
let bin2 = 0b1010; // 10
// Octal (base 8)
let oct = 0o377; // 255
// Scientific notation
let big = 1e6; // 1,000,000
let small = 1.5e-3; // 0.0015
// Numeric separators (ES2021) - underscore for readability
let million = 1_000_000; // 1000000
let price = 19_99; // 1999 (cents)
let hex3 = 0xFF_00_FF; // useful for hex colors
console.log(a === hex); // true (both are 255)
console.log(million); // 1000000
The numeric separator (_) is particularly nice for making large numbers readable. 1_000_000 is so much easier to read than 1000000. This is a quality-of-life feature that came in ES2021.
09Putting It All Together: A Practical Number-Handling Example
Let's write something realistic. Say you're building a shopping cart and need to calculate a subtotal, apply a discount, add tax, and display everything nicely. This hits most of what we covered:
function calculateCartTotal(items, discountPercent, taxRate) {
// Step 1: Sum up item prices (handle potential non-numbers safely)
const subtotal = items.reduce((sum, item) => {
const price = parseFloat(item.price);
const qty = parseInt(item.quantity, 10);
if (Number.isNaN(price) || Number.isNaN(qty)) {
console.warn("Invalid item data:", item);
return sum;
}
return sum + price * qty;
}, 0);
// Step 2: Apply discount (work in integers to avoid float issues)
const discountAmount = Math.round(subtotal * (discountPercent / 100) * 100) / 100;
const afterDiscount = Math.round((subtotal - discountAmount) * 100) / 100;
// Step 3: Add tax
const taxAmount = Math.round(afterDiscount * taxRate * 100) / 100;
const total = Math.round((afterDiscount + taxAmount) * 100) / 100;
return {
subtotal: subtotal.toFixed(2),
discount: discountAmount.toFixed(2),
tax: taxAmount.toFixed(2),
total: total.toFixed(2),
};
}
const cart = [
{ name: "Keyboard", price: "79.99", quantity: "1" },
{ name: "Mouse", price: "34.50", quantity: "2" },
{ name: "USB Hub", price: "24.99", quantity: "1" },
];
const result = calculateCartTotal(cart, 10, 0.08);
console.log("Subtotal: $" + result.subtotal); // $173.98
console.log("Discount: $" + result.discount); // $17.40
console.log("Tax: $" + result.tax); // $12.69
console.log("Total: $" + result.total); // $186.27
Notice how we use parseFloat() and parseInt() because the prices and quantities are coming in as strings (from a form or API). We check for NaN before using values. We multiply-then-divide to handle floating point precision. And we use toFixed(2) only at the display stage.
That's professional-grade number handling right there.
10Quick Reference: JavaScript Number Essentials
| Constant | Value | Use It When |
|---|---|---|
| Number.MAX_SAFE_INTEGER | 9007199254740991 | Checking if a number is safe to work with |
| Number.MIN_SAFE_INTEGER | -9007199254740991 | Lower boundary of safe integers |
| Number.MAX_VALUE | ~1.8 × 10³⁰⁸ | Largest possible JS number before Infinity |
| Number.EPSILON | ~2.22 × 10⁻¹⁶ | Comparing floats for near-equality |
| Number.POSITIVE_INFINITY | Infinity | Same as the global Infinity constant |
| Number.NaN | NaN | Same as the global NaN constant |
11What This Connects to in the Series
Numbers sit right at the center of so much JavaScript work. If you haven't read about how variables work yet, that's a good foundation to have before going deeper. The data types article is the broader context that numbers fit into, and the strings article is the natural companion to this one since strings and numbers constantly interact through parsing and conversion.
Coming up next: we're covering Booleans, null, and undefined, which is where we get into the infamous "falsy values" list in JavaScript, and finally settle the age-old question of what the actual difference between null and undefined is.
If numbers felt like a lot today, take a moment and play with the interactive demos above. Type things in your browser console. Break things on purpose. That's genuinely the fastest way to internalize how JavaScript handles numeric values. The floating-point surprise, the NaN contagion, the BigInt n-suffix: these stick when you see them in action, not just on a screen reading about them.
You've got this.
Numbers in JavaScript
Loading challenge…
Ready to Practice?
Select a difficulty level above to load your challenge.