Numbers and Math
Understand how JavaScript represents numbers, work with the Number and Math built-ins, round values correctly, and recognize the limits of floating-point precision.
JavaScript has only one numeric type for ordinary numbers (there is no separate int vs float), which makes arithmetic easy to write but occasionally surprising. This lesson covers the Number and Math objects you'll reach for constantly, plus the floating-point quirks every JavaScript developer eventually runs into.
The Number type
Every number in JavaScript — whole or decimal, positive or negative — is stored as a 64-bit floating-point value (the IEEE 754 double-precision format). There's no separate integer type, so 5 and 5.0 are literally the same value.
This single-type design keeps arithmetic simple to write, but it means very large integers (beyond about 2^53) and certain decimal fractions cannot be represented exactly — more on that below.
NaN and Infinity
NaN stands for "Not a Number" and is the special value produced when a numeric operation fails to produce a valid number, such as 0 / 0 or Number("abc"). Oddly, typeof NaN is "number", and NaN === NaN is always false — you must use Number.isNaN(value) to check for it, never value === NaN.
Infinity and -Infinity represent values beyond the largest number JavaScript can represent, produced for example by 1 / 0. You can test for them with Number.isFinite(value), which returns false for NaN and both infinities.
Useful Number methods
The Number object provides both conversion helpers and validity checks. Number(value) converts a string or other value to a number (returning NaN if it can't), while parseInt() and parseFloat() read a number from the start of a string and ignore trailing non-numeric text.
Number.isInteger(value) checks whether a value is a whole number, and toFixed(digits) formats a number as a string with a fixed number of decimal places — commonly used for displaying currency.
• Number("42") → 42, Number("42px") → NaN • parseInt("42px") → 42 (stops at the first non-digit) • Number.isInteger(4.0) → true, Number.isInteger(4.5) → false • (3.14159).toFixed(2) → "3.14" (returns a string, not a number)
The Math object
Math is a built-in object (not a constructor — you never write new Math()) that bundles constants and functions for common numeric operations: Math.round(), Math.floor(), Math.ceil(), Math.abs(), Math.max(), Math.min(), Math.random(), and Math.pow() among many others.
Math.random() returns a pseudo-random floating-point number in the range [0, 1) — greater than or equal to 0, and strictly less than 1. To get a random integer in a specific range, you scale and floor it, a pattern worth memorizing.
Rounding correctly
Math.round(x) rounds to the nearest integer (rounding .5 up), Math.floor(x) always rounds down toward negative infinity, and Math.ceil(x) always rounds up toward positive infinity. These behave differently on negative numbers: Math.floor(-2.1) is -3, not -2.
For a fixed number of decimal places (like currency), toFixed() is usually more appropriate than combining Math.round with multiplication, since manual scaling can reintroduce floating-point errors.
Floating-point precision
Because numbers are stored in binary floating-point, some decimal fractions cannot be represented exactly — the classic example is 0.1 + 0.2, which evaluates to 0.30000000000000004, not 0.3. This is not a JavaScript bug; nearly every language using IEEE 754 floats has the same behavior.
The practical fix is to never compare floating-point numbers with === when they result from arithmetic — instead check that the difference is smaller than a tiny tolerance (an "epsilon"), or work in integer units (like cents instead of dollars) when precision really matters.
Number and Math essentials
toFixed() returns a string, so if you need a number afterward, wrap it with Number(...) again. Math.random() never returns exactly 1, which is why + 1 and Math.floor() together give an inclusive integer range. Number.isNaN() is safer than the global isNaN(), which coerces its argument and can give misleading results (isNaN("abc") is true even though "abc" was never a number to begin with).
Number("3.14"); // 3.14
Number.isNaN(NaN); // true
Number.isInteger(10); // true
(19.995).toFixed(2); // "20.00" (rounds, returns a string)
Math.round(4.5); // 5
Math.floor(4.9); // 4
Math.ceil(4.1); // 5
Math.max(3, 7, 2); // 7
Math.abs(-9); // 9
// Random integer between min and max (inclusive)
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}Code Examples
A classic demonstration of floating-point imprecision: the sum looks close to 0.3 but is not exactly equal to it in binary floating-point representation. Expected result: 0.30000000000000004 false
0.1 + 0.2 === 0.3 evaluates to false because the stored binary approximations don't add up to exactly the binary approximation of 0.3.
Common mistake: Never assume two computed decimal numbers are exactly equal — compare with a small tolerance instead.
Instead of comparing floating-point results with strict equality, this checks that the two values are close enough to be considered equal for practical purposes. Expected result: true false
Math.abs(a - b) < epsilon treats two numbers as equal if their difference is smaller than a tiny threshold.
Money is stored in whole cents (integers) to avoid floating-point errors, then converted to a dollar display string only at the final formatting step. rollDice shows the standard random-integer-in-range pattern. Expected result: $19.99 $5.00 4 (varies each run) Storing money as integer cents rather than decimal dollars sidesteps floating-point rounding errors entirely, which is why many payment systems (like Stripe) use this convention internally.
Math.floor(Math.random() * 6) + 1 maps the range [0, 1) to the whole numbers 1 through 6.
console.log(0.1 + 0.2);
console.log(0.1 + 0.2 === 0.3);function nearlyEqual(a, b, epsilon = 0.0001) {
return Math.abs(a - b) < epsilon;
}
console.log(nearlyEqual(0.1 + 0.2, 0.3));
console.log(0.1 + 0.2 === 0.3);function formatPrice(cents) {
const dollars = cents / 100;
return "$" + dollars.toFixed(2);
}
console.log(formatPrice(1999));
console.log(formatPrice(500));
function rollDice() {
return Math.floor(Math.random() * 6) + 1;
}
console.log(rollDice()); // a random integer 1 through 6Important Rules
JavaScript has a single Number type for both integers and decimals — there is no separate int type.
Always check for NaN with Number.isNaN(value), never with value === NaN, since NaN is never equal to anything, including itself.
Never compare the results of floating-point arithmetic with === — use a small epsilon tolerance instead.
Math.floor, Math.ceil, and Math.round behave differently on negative numbers — test edge cases if your values might be negative.
toFixed() returns a string; wrap the result in Number(...) if you need to keep doing arithmetic with it.
Where this shows up
Every shopping cart total, tax calculation, or invoice amount depends on careful numeric handling. Games and animations lean on Math.random() and Math.floor() constantly for dice rolls, random spawn positions, and shuffling. UI code uses Math.min/Math.max to clamp values (like a volume slider) within a valid range.
if (value === NaN) { /* never true */ } if (Number.isNaN(value)) { /* correct check */ }
NaN is defined to never equal anything, including itself, so value === NaN always evaluates to false regardless of value. Number.isNaN() is the only reliable way to detect it.
const total = 19.99 + 0.01; // 20.000000000000004 const totalCents = 1999 + 1; // 2000, then divide/format only for display
Doing repeated decimal arithmetic on money accumulates floating-point rounding error. Working in the smallest whole unit (cents) and converting to dollars only when displaying avoids the problem.
[Multiple Choice] Which is the correct way to check whether a value is NaN? A. value === NaN B. Number.isNaN(value) C. typeof value === "NaN" D. value == "NaN"
B. Number.isNaN(value) — NaN never equals anything, including itself, so === and == comparisons always fail. Number.isNaN() is the dedicated, reliable check.
[True / False] In JavaScript, 0.1 + 0.2 is exactly equal to 0.3. A. True B. False
B. False — False. Due to binary floating-point representation, 0.1 + 0.2 actually equals 0.30000000000000004, which is not strictly equal to 0.3.
Write a function clamp(value, min, max) that returns value if it's within the range [min, max], but returns min if value is too low or max if value is too high. Test it with clamp(15, 0, 10) and clamp(-5, 0, 10).
function clamp(value, min, max) { return Math.max(min, Math.min(value, max)); } console.log(clamp(15, 0, 10)); // 10 console.log(clamp(-5, 0, 10)); // 0
Math.max(min, Math.min(value, max)) clamps a value in one expression.