JavaScript Numbers & Math: Precision, Methods, and the Math Object
JavaScript has only one number type for both integers and decimals: the 64-bit IEEE 754 double-precision floating-point format. This design makes JavaScript simple but introduces precision quirks that every developer must understand. Let us explore numbers from the ground up.
How JavaScript numbers work
Every number in JavaScript — whether 1, 3.14, or -42 — is stored as a 64-bit floating-point number. This gives you roughly 15-17 significant digits of precision and a range from roughly ±5 × 10-324 to ±1.7976 × 10308.
// All of these are the same type
typeof 42 // 'number'
typeof 3.14 // 'number'
typeof -0 // 'number'
typeof Infinity // 'number'
typeof NaN // 'number' (yes, "Not a Number" is a number!)
// Number literals
const decimal = 42;
const float = 3.14;
const negative = -7;
const scientific = 2.5e6; // 2,500,000
const tiny = 1.5e-3; // 0.0015
const hex = 0xFF; // 255
const octal = 0o77; // 63
const binary = 0b1010; // 10
const withSeparator = 1_000_000; // 1000000 (numeric separator for readability)
The floating-point precision problem
This is the most famous quirk in JavaScript — and in every language that uses IEEE 754.
// The classic problem
0.1 + 0.2 // 0.30000000000000004 (not 0.3!)
0.1 + 0.2 === 0.3 // false!
// Why? Because 0.1 and 0.2 cannot be represented exactly in binary
// Just like 1/3 = 0.333... in decimal, 0.1 = 0.0001100110011... in binary
// More examples
0.3 - 0.1 // 0.19999999999999998
0.1 * 3 // 0.30000000000000004
1.005 * 100 // 100.49999999999999 (not 100.5!)
// How to handle this:
// 1. Use Number.EPSILON for comparison
function areEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
areEqual(0.1 + 0.2, 0.3) // true
// 2. Work in integers (cents instead of dollars)
const priceInCents = 1099; // $10.99
const tax = Math.round(priceInCents * 0.08); // 88 cents
const total = priceInCents + tax; // 1187 cents = $11.87
// 3. Use toFixed for display (returns a string!)
(0.1 + 0.2).toFixed(2) // '0.30'
(1.005).toFixed(2) // '1.00' (not '1.01'! Another precision issue)
// Fix toFixed rounding
function roundTo(num, decimals) {
return Number(Math.round(num + 'e' + decimals) + 'e-' + decimals);
}
roundTo(1.005, 2) // 1.01 (correct!)
Special number values
// Infinity
1 / 0 // Infinity
-1 / 0 // -Infinity
Infinity + 1 // Infinity
Infinity - Infinity // NaN
Infinity * 0 // NaN
Math.max() // -Infinity (no arguments)
Math.min() // Infinity
// NaN — Not a Number
0 / 0 // NaN
parseInt('hello') // NaN
Math.sqrt(-1) // NaN
undefined + 1 // NaN
NaN + 5 // NaN (any operation with NaN gives NaN)
NaN === NaN // false (NaN is never equal to itself!)
NaN !== NaN // true (the only value that is not equal to itself)
// How to check for NaN
Number.isNaN(NaN) // true
Number.isNaN('hello') // false (does not coerce!)
isNaN('hello') // true (coerces to NaN first — misleading!)
// Positive and negative zero
0 === -0 // true (they are "equal")
1 / 0 // Infinity
1 / -0 // -Infinity (they behave differently!)
Object.is(0, -0) // false (Object.is can distinguish them)
// Safe integer range
Number.MAX_SAFE_INTEGER // 9007199254740991 (2^53 - 1)
Number.MIN_SAFE_INTEGER // -9007199254740991
Number.isSafeInteger(9007199254740991) // true
Number.isSafeInteger(9007199254740992) // false
// Beyond safe range, precision is lost
9007199254740992 === 9007199254740993 // true! (cannot distinguish them)
Number methods
// Number.isFinite() — is it a real, finite number?
Number.isFinite(42) // true
Number.isFinite(3.14) // true
Number.isFinite(Infinity) // false
Number.isFinite(NaN) // false
Number.isFinite('42') // false (no coercion!)
isFinite('42') // true (global version coerces — avoid!)
// Number.isInteger()
Number.isInteger(5) // true
Number.isInteger(5.0) // true (5.0 === 5 in JS)
Number.isInteger(5.5) // false
Number.isInteger('5') // false
// Number.isNaN()
Number.isNaN(NaN) // true
Number.isNaN(42) // false
Number.isNaN('NaN') // false (string, not NaN)
// Number.parseFloat() / Number.parseInt()
// Same as global parseFloat/parseInt
Number.parseFloat('3.14px') // 3.14
Number.parseInt('FF', 16) // 255
Converting numbers to strings
// toString(radix) — convert to different bases
(255).toString(16) // 'ff' (hexadecimal)
(255).toString(2) // '11111111' (binary)
(255).toString(8) // '377' (octal)
(42).toString() // '42' (decimal, default)
// toFixed(digits) — fixed decimal places (returns string!)
(3.14159).toFixed(2) // '3.14'
(3).toFixed(2) // '3.00'
(0.1 + 0.2).toFixed(1) // '0.3'
// toPrecision(digits) — total significant digits
(123.456).toPrecision(5) // '123.46'
(123.456).toPrecision(2) // '1.2e+2'
(0.00123).toPrecision(2) // '0.0012'
// toExponential(digits)
(123456).toExponential(2) // '1.23e+5'
(0.00456).toExponential(1) // '4.6e-3'
The Math object
The Math object is a built-in namespace with constants and static methods for mathematical operations. It is not a constructor — you do not use new Math().
Constants
Math.PI // 3.141592653589793
Math.E // 2.718281828459045 (Euler's number)
Math.LN2 // 0.693... (natural log of 2)
Math.LN10 // 2.302... (natural log of 10)
Math.LOG2E // 1.442... (log base 2 of E)
Math.LOG10E // 0.434... (log base 10 of E)
Math.SQRT2 // 1.414... (square root of 2)
Math.SQRT1_2 // 0.707... (square root of 1/2)
Rounding methods
// Math.round() — round to nearest integer (0.5 rounds up)
Math.round(4.5) // 5
Math.round(4.4) // 4
Math.round(-4.5) // -4 (rounds toward +Infinity)
// Math.floor() — round DOWN (toward -Infinity)
Math.floor(4.9) // 4
Math.floor(-4.1) // -5
// Math.ceil() — round UP (toward +Infinity)
Math.ceil(4.1) // 5
Math.ceil(-4.9) // -4
// Math.trunc() — remove decimal part (toward zero)
Math.trunc(4.9) // 4
Math.trunc(-4.9) // -4 (unlike floor!)
// Practical: round to N decimal places
function round(num, decimals) {
const factor = 10 ** decimals;
return Math.round(num * factor) / factor;
}
round(3.14159, 2) // 3.14
round(1.005, 2) // 1 (precision issue! Use the roundTo function above)
Min, max, and clamping
// Math.min() / Math.max()
Math.min(1, 2, 3) // 1
Math.max(1, 2, 3) // 3
Math.min() // Infinity (!)
Math.max() // -Infinity (!)
// With arrays — use spread
const nums = [5, 2, 8, 1, 9];
Math.min(...nums) // 1
Math.max(...nums) // 9
// Clamping a value to a range
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
clamp(15, 0, 10) // 10
clamp(-5, 0, 10) // 0
clamp(5, 0, 10) // 5
// Math.clamp is proposed but not yet available
Powers, roots, and logarithms
// Math.pow() and ** operator
Math.pow(2, 10) // 1024
2 ** 10 // 1024 (same, but cleaner syntax)
Math.pow(27, 1/3) // 3 (cube root)
// Math.sqrt() / Math.cbrt()
Math.sqrt(144) // 12
Math.cbrt(27) // 3
// Math.hypot() — distance formula
Math.hypot(3, 4) // 5 (sqrt(9 + 16))
Math.hypot(1, 2, 3) // sqrt(1 + 4 + 9) = 3.741...
// Math.log() — natural logarithm (base e)
Math.log(Math.E) // 1
Math.log(1) // 0
Math.log(0) // -Infinity
// Math.log2() / Math.log10()
Math.log2(1024) // 10
Math.log10(1000) // 3
Trigonometry
// Arguments are in RADIANS, not degrees
Math.sin(0) // 0
Math.cos(0) // 1
Math.tan(Math.PI / 4) // 0.9999... (approximately 1)
// Convert degrees to radians
const toRadians = degrees => degrees * (Math.PI / 180);
const toDegrees = radians => radians * (180 / Math.PI);
Math.sin(toRadians(30)) // 0.5
Math.sin(toRadians(90)) // 1
// Inverse trig
Math.asin(1) // 1.5707... (PI/2)
Math.atan2(1, 1) // 0.7853... (PI/4 = 45 degrees)
// atan2 is essential for calculating angles between points
const angle = Math.atan2(dy, dx); // angle in radians
Random numbers
// Math.random() — returns [0, 1)
Math.random() // 0.7234... (different each time)
// Random integer in range [min, max] (inclusive)
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
randomInt(1, 6) // 1-6 (dice roll)
randomInt(0, 255) // 0-255 (RGB component)
// Random element from array
function randomElement(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
randomElement(['red', 'green', 'blue']) // random color
// Shuffle an array (Fisher-Yates algorithm)
function shuffle(arr) {
const result = [...arr];
for (let i = result.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[result[i], result[j]] = [result[j], result[i]];
}
return result;
}
shuffle([1, 2, 3, 4, 5]) // [3, 1, 5, 2, 4] (random order)
// Random hex color
function randomColor() {
return '#' + Math.floor(Math.random() * 0xFFFFFF).toString(16).padStart(6, '0');
}
randomColor() // '#a3f2c1'
// Note: Math.random() is NOT cryptographically secure!
// For security, use crypto.getRandomValues()
const secureRandom = new Uint32Array(1);
crypto.getRandomValues(secureRandom);
Other Math methods
// Math.abs() — absolute value
Math.abs(-42) // 42
Math.abs(42) // 42
// Math.sign() — returns -1, 0, or 1
Math.sign(-42) // -1
Math.sign(0) // 0
Math.sign(42) // 1
// Math.fround() — nearest 32-bit float
Math.fround(1.337) // 1.3370000123977661
// Math.imul() — 32-bit integer multiplication
Math.imul(2, 4) // 8
BigInt — numbers beyond safe integers
When you need integers larger than 2^53 - 1, use BigInt. This is a separate type, not a regular number.
// Create BigInt with n suffix or BigInt() constructor
const big = 9007199254740993n;
const alsoBig = BigInt('9007199254740993');
const fromNum = BigInt(42); // 42n
// Arithmetic works as expected
big + 1n // 9007199254740994n
1000n * 1000n * 1000n * 1000n // 1000000000000n
2n ** 100n // 1267650600228229401496703205376n
// But you CANNOT mix BigInt and Number
// big + 1 // TypeError!
// Math.sqrt(4n) // TypeError!
// Convert between types explicitly
Number(42n) // 42
BigInt(42) // 42n
// Comparison works across types
42n === 42 // false (different types)
42n == 42 // true (loose equality coerces)
42n > 41 // true
// Division truncates (no decimals in BigInt)
7n / 2n // 3n (not 3.5n)
// typeof
typeof 42n // 'bigint'
Formatting numbers for display
// Intl.NumberFormat — the professional way to format numbers
const formatter = new Intl.NumberFormat('en-US');
formatter.format(1234567.89) // '1,234,567.89'
// Currency
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(42.5) // '$42.50'
new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR'
}).format(42.5) // '42,50 €'
// Percentage
new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: 1
}).format(0.856) // '85.6%'
// Compact notation
new Intl.NumberFormat('en-US', {
notation: 'compact',
maximumFractionDigits: 1
}).format(1234567) // '1.2M'
// Units
new Intl.NumberFormat('en-US', {
style: 'unit',
unit: 'kilometer-per-hour'
}).format(120) // '120 km/h'
// Custom formatting
function formatBytes(bytes) {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let i = 0;
while (bytes >= 1024 && i < units.length - 1) {
bytes /= 1024;
i++;
}
return bytes.toFixed(1) + ' ' + units[i];
}
formatBytes(1536) // '1.5 KB'
formatBytes(1073741824) // '1.0 GB'
Numbers in JavaScript are both simpler and trickier than they first appear. One type for everything is convenient, but the floating-point precision issues and the special values require careful handling. Use the right tool — Number for everyday math, BigInt for large integers, Intl.NumberFormat for display, and always be aware of precision when dealing with money or exact calculations.