Almost every program does arithmetic somewhere: a cart total, a percentage, a price with tax, a random pick from a list. JavaScript handles all of that with a single number type — and that one decision shapes how numbers behave, including one quirk that catches nearly everyone the first time they see it. In this article you’ll learn how numbers actually work under the hood, the floating-point trap and how to dodge it, and the built-in Number and Math tools you’ll reach for constantly.
You met numbers briefly back in the data types article. Now we’ll go deep enough that numeric bugs stop being mysterious.
One number type for everything
Most languages split numbers into separate types — integers, floats, doubles, and so on. JavaScript keeps it simple: there’s just one type, called number, and it covers whole numbers and decimals alike.
const age = 28; // a whole number
const price = 19.99; // a decimal
const negative = -7; // negative is fine too
const temperature = -3.5;
You don’t declare a separate “integer” or “float” — you just write the value. Under the hood, every JavaScript number is a 64-bit floating-point value (the IEEE 754 “double” format, if you ever want to look it up). That’s convenient, but it’s also the root of the most famous gotcha in the language, which we’ll get to in a moment.
You can also write numbers in a few other notations when it’s handy:
const big = 1_000_000; // underscores as digit separators (just for readability)
const hex = 0xff; // hexadecimal → 255
const binary = 0b1010; // binary → 10
const octal = 0o17; // octal → 15
const scientific = 1.5e6; // scientific notation → 1500000
The underscores are purely visual — JavaScript ignores them, so 1_000_000 and 1000000 are the exact same number. They just make long numbers easier to read.
The floating-point gotcha
Here’s the one that trips up every newcomer. Try this:
console.log(0.1 + 0.2); // 0.30000000000000004 (!)
console.log(0.1 + 0.2 === 0.3); // false
That’s not a bug in JavaScript — it happens in almost every language that uses the same floating-point standard (Python, Java, C, and more). The reason: computers store numbers in binary (base 2), and just like 1/3 can’t be written exactly as a decimal (0.3333…), the value 0.1 can’t be represented exactly in binary. The tiny rounding error is what leaks out at the far end of the decimal.
Never compare decimals with === directly
Because of floating-point rounding, 0.1 + 0.2 === 0.3 is false. Don’t compare decimal results with === and expect exact matches. When you need to check whether two decimals are “equal enough,” compare the difference against a tiny tolerance instead:
const a = 0.1 + 0.2;
const b = 0.3;
console.log(Math.abs(a - b) < Number.EPSILON); // trueNumber.EPSILON is the smallest meaningful gap between numbers JavaScript can tell apart — a ready-made tolerance for exactly this situation.
This matters most with money. Storing prices as floating-point decimals and adding them up can slowly accumulate rounding errors. A common professional trick is to work in the smallest unit instead — store amounts as whole cents (integers) rather than dollars with decimals, then divide by 100 only when you display the value. Integers don’t suffer the same rounding problem.
// Instead of dollars with decimals:
const total = 19.99 + 5.01; // risk of a rounding tail
// Work in cents (integers), divide at the end:
const totalCents = 1999 + 501; // 2500
const display = (totalCents / 100).toFixed(2); // "25.00"
Useful Number methods
The number type comes with handy methods for formatting and inspecting values. The two you’ll use most are toFixed and toString.
toFixed — fix the number of decimals
toFixed(n) rounds a number to n decimal places and returns it as a string — perfect for displaying prices and percentages:
const price = 19.9;
console.log(price.toFixed(2)); // "19.90" (a string)
const pi = 3.14159;
console.log(pi.toFixed(2)); // "3.14"
console.log((1234.5678).toFixed(0)); // "1235" (rounds)
toFixed returns a string, not a number
This catches people often: (19.9).toFixed(2) gives you the string "19.90", not the number 19.9. That’s usually what you want for display — it keeps trailing zeros so a price shows as 19.90 and not 19.9. But if you try to do math on the result, JavaScript will treat the + as string joining: "19.90" + 1 becomes "19.901", not 20.9. Format for display after you finish calculating, never before.
toString — convert to other bases
toString() turns a number into a string, and you can pass a base (radix) to convert between number systems:
const n = 255;
console.log(n.toString()); // "255" (base 10, the default)
console.log(n.toString(16)); // "ff" (hexadecimal)
console.log(n.toString(2)); // "11111111" (binary)
This is genuinely useful — converting a number to base 16 is exactly how you’d build a hex color code from RGB values, for example.
toLocaleString — format for humans
toLocaleString() formats a number the way a reader in a given region expects, with the right grouping separators:
const amount = 1234567.89;
console.log(amount.toLocaleString("en-US")); // "1,234,567.89"
console.log(amount.toLocaleString("id-ID")); // "1.234.567,89"
// Even better — format as currency directly:
console.log(amount.toLocaleString("id-ID", {
style: "currency",
currency: "IDR",
})); // "Rp1.234.567,89"
This is the right tool for showing money and large counts to users. It handles thousand separators, decimal commas vs. dots, and currency symbols automatically, based on the locale — far better than gluing commas in by hand.
Checking numbers: NaN and Infinity
Numbers have two special values worth knowing, because they show up when math goes sideways.
NaN — Not a Number
NaN means “Not a Number.” You get it when an operation that expects numbers can’t produce a sensible numeric result:
console.log("hello" * 2); // NaN
console.log(Number("abc")); // NaN
console.log(0 / 0); // NaN
NaN has a famously weird property: it is not equal to anything, including itself.
console.log(NaN === NaN); // false (!)
So you can’t check for it with ===. Instead, use Number.isNaN():
const result = Number("abc"); // NaN
console.log(Number.isNaN(result)); // true
Use Number.isNaN, not the old global isNaN
There’s an older global function called isNaN() (without the Number. prefix). Avoid it — it first tries to convert its argument to a number, so isNaN("hello") returns true even though the string was never a number to begin with. Number.isNaN() does no conversion: it returns true only for a value that is genuinely the NaN number. Reach for Number.isNaN() and you’ll dodge a class of subtle bugs.
Infinity
Dividing by zero doesn’t crash in JavaScript — it gives you Infinity (or -Infinity):
console.log(1 / 0); // Infinity
console.log(-1 / 0); // -Infinity
console.log(1 / 0 === Infinity); // true
To confirm a value is a real, usable number — not NaN, not Infinity — use Number.isFinite():
console.log(Number.isFinite(42)); // true
console.log(Number.isFinite(Infinity)); // false
console.log(Number.isFinite(NaN)); // false
This is a great guard before trusting the result of a calculation or a parsed value.
Turning strings into numbers
Data from forms, URLs, and APIs often arrives as text — "42", not 42. You’ll constantly need to convert. There are three common ways, and they behave differently.
// 1. Number() — strict, whole-string conversion
console.log(Number("42")); // 42
console.log(Number("42px")); // NaN (the "px" ruins it)
console.log(Number("3.14")); // 3.14
// 2. parseInt() — reads an integer from the FRONT, stops at junk
console.log(parseInt("42px")); // 42 (ignores "px")
console.log(parseInt("3.14")); // 3 (stops at the dot)
// 3. parseFloat() — like parseInt but keeps the decimal
console.log(parseFloat("3.14")); // 3.14
console.log(parseFloat("3.14m"));// 3.14 (ignores "m")
The difference matters. Number() is all-or-nothing: the whole string must look like a number or you get NaN. parseInt() and parseFloat() are more forgiving — they read as much of a number as they can from the start and quietly stop at the first character that doesn’t fit. That’s exactly what you want for something like "42px" (where you mean 42), and exactly what you don’t want when you’d rather catch bad input.
Always pass a radix to parseInt
Make a habit of giving parseInt a second argument — the base — like parseInt("42", 10). Without it, certain inputs (historically, strings starting with 0) could be interpreted in an unexpected base. Modern engines default to base 10 in most cases, but writing parseInt(value, 10) makes your intent explicit and removes any doubt. It costs you four characters and removes a whole category of surprise.
A quick shortcut you’ll see in real code: the unary + operator converts a string to a number, the same way Number() does.
const text = "100";
const num = +text; // 100, as a number
console.log(num + 5); // 105
The Math object
For anything beyond basic arithmetic, JavaScript gives you the built-in Math object — a collection of math constants and functions. You don’t create it; it’s always there. You call its methods directly off Math.
Rounding
The most-used Math methods round numbers in different ways:
console.log(Math.round(4.5)); // 5 (nearest integer)
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 (just drops the decimal)
console.log(Math.trunc(-4.9)); // -4 (toward zero)
It’s worth keeping these straight: round goes to the nearest, ceil always goes up, floor always goes down, and trunc simply chops off the decimal part without rounding at all. For negative numbers, floor and trunc differ — Math.floor(-4.9) is -5, but Math.trunc(-4.9) is -4.
Common calculations
A handful of Math methods come up again and again:
console.log(Math.abs(-7)); // 7 (absolute value)
console.log(Math.pow(2, 10)); // 1024 (2 to the 10th)
console.log(2 ** 10); // 1024 (the ** operator — same thing)
console.log(Math.sqrt(144)); // 12 (square root)
console.log(Math.max(3, 9, 1)); // 9 (largest)
console.log(Math.min(3, 9, 1)); // 1 (smallest)
The ** exponent operator does the same job as Math.pow and reads a little cleaner, so you’ll often prefer it. And Math.max/Math.min take any number of arguments — handy for finding the highest score or the lowest price in a set.
Find the max of an array with the spread operator
Math.max takes a list of numbers, not an array. If you already have an array, spread it in with ...:
const scores = [82, 91, 67, 100, 73];
console.log(Math.max(...scores)); // 100
console.log(Math.min(...scores)); // 67The ... “spreads” the array’s items into separate arguments, as if you’d typed Math.max(82, 91, 67, 100, 73). It’s the standard way to get the biggest or smallest value out of an array.
Math constants
Math also holds a few useful constants:
console.log(Math.PI); // 3.141592653589793
console.log(Math.E); // 2.718281828459045
// Area of a circle with radius 5:
const radius = 5;
const area = Math.PI * radius ** 2;
console.log(area.toFixed(2)); // "78.54"
Random numbers
Math.random() returns a random decimal between 0 (inclusive) and 1 (exclusive) — so somewhere from 0 up to, but never reaching, 1:
console.log(Math.random()); // e.g. 0.7263… (different every time)
On its own that’s rarely what you want. The common task is a random integer in a range, and there’s a standard recipe for it:
// Random integer from min to max (both inclusive):
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
console.log(randomInt(1, 6)); // a dice roll: 1–6
console.log(randomInt(1, 100)); // 1–100
Read it from the inside out: Math.random() gives a fraction, multiplying scales it up to the size of your range, Math.floor chops it down to a whole number, and adding min shifts it to start at the right place. Picking a random item from an array uses the same idea:
const colors = ["red", "green", "blue"];
const pick = colors[Math.floor(Math.random() * colors.length)];
console.log(pick); // one of the three, at random
Math.random is not secure
Math.random() is fine for games, shuffles, and picking a random tip to show — anything where unpredictability is “nice to have.” It is not cryptographically secure, so never use it to generate passwords, session tokens, or anything security-sensitive. For that, browsers provide crypto.getRandomValues(), which is built for unpredictability. Match the tool to the stakes.
When integers aren’t enough: BigInt
Regular numbers are safe and exact only up to a limit — Number.MAX_SAFE_INTEGER, which is 9007199254740991 (just over 9 quadrillion). Past that point, integers can quietly lose precision:
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(9007199254740991 + 1); // 9007199254740992 (ok)
console.log(9007199254740991 + 2); // 9007199254740992 (wrong! should be ...993)
For the rare cases where you genuinely need huge whole numbers — large database IDs, certain financial or scientific work — JavaScript has BigInt. You make one by adding n to the end of an integer literal:
const huge = 9007199254740991n;
console.log(huge + 2n); // 9007199254740993n (correct!)
The catch: you can’t mix BigInt and regular numbers in the same operation (10n + 5 throws an error — you’d write 10n + 5n), and BigInt can’t hold decimals. For everyday work, stick with normal numbers; reach for BigInt only when you actually bump into the safe-integer limit.
A realistic example
Let’s pull several of these together into something you might actually write — a small order summary:
const order = {
customer: "John Doe",
items: [
{ name: "Keyboard", price: 250000, qty: 1 },
{ name: "Mouse", price: 120000, qty: 2 },
],
};
// Sum each item's price × quantity
let subtotal = 0;
for (const item of order.items) {
subtotal += item.price * item.qty;
}
const taxRate = 0.11; // 11% tax
const tax = subtotal * taxRate;
const total = subtotal + tax;
console.log("Subtotal:", subtotal.toLocaleString("id-ID")); // 490.000
console.log("Tax:", Math.round(tax).toLocaleString("id-ID")); // 53.900
console.log("Total:", Math.round(total).toLocaleString("id-ID")); // 543.900
This is numbers doing real work: a loop to add things up (the for...of from the loops article), arithmetic for the tax, Math.round to clean up the result, and toLocaleString to present it the way a person expects to read it.
Wrapping up
You now have a solid grip on how numbers behave in JavaScript and the tools that go with them:
- JavaScript has one number type for integers and decimals alike. Under the hood it’s 64-bit floating-point — convenient, but the source of one classic quirk.
- Decimal math isn’t exact (
0.1 + 0.2 !== 0.3). Don’t compare decimals with===; for money, work in the smallest unit (cents) as integers. - Number methods format and convert:
toFixed(n)for fixed decimals (returns a string),toString(radix)for other bases,toLocaleString()for human-friendly and currency formatting. NaNmeans “not a number” and equals nothing, even itself — check withNumber.isNaN(). Dividing by zero givesInfinity— guard real results withNumber.isFinite().- Convert strings with
Number()(strict),parseInt(str, 10), orparseFloat()(lenient). Always pass a radix toparseInt. - The
Mathobject handles the rest:round/ceil/floor/truncfor rounding,abs/sqrt/max/min/**for calculation, andMath.random()for randomness (not for security). - For whole numbers beyond
Number.MAX_SAFE_INTEGER, reach forBigInt(123n).
Numbers and text are the two raw materials almost every program is built from. You’ve now covered both the numeric side and, in the basics of strings and data types, the textual side. Next up, we’ll look at dates and times — which, as it happens, are stored as numbers too, so everything you just learned carries straight over.