JavaScript Numbers, Math, and BigInt: IEEE 754, Precision, and When to Use What

Link copied
JavaScript Numbers, Math, and BigInt: IEEE 754, Precision, and When to Use What

JavaScript Numbers, Math, and BigInt: IEEE 754, Precision, and When to Use What

JavaScript has exactly one numeric type — number — for both integers and decimals. Underneath, it's a 64-bit IEEE 754 double-precision float. That single decision shapes everything about how numbers behave: why 0.1 + 0.2 is not 0.3, why integers above 2⁵³ are unsafe, why BigInt exists, and why financial code in JavaScript reaches for libraries instead of plain arithmetic.

This lesson covers the mechanics, the practical gotchas, the Math API, and BigInt — the second numeric type the language picked up in 2020.

What number actually is #

A 64-bit IEEE 754 double-precision floating-point number. The same format used for double in most languages.

Key properties:

  • 53 bits of precision for the mantissa (significant digits)
  • 11 bits for the exponent
  • 1 bit for the sign
  • Can represent integers exactly up to ±2⁵³ – 1 (Number.MAX_SAFE_INTEGER = 9007199254740991)
  • Can represent reals as close as the format allows — but most decimals are not exactly representable

This is the same model as Java's double, Python's float, and almost every other modern language's default decimal type.

The famous gotcha: 0.1 + 0.2 #

0.1 + 0.2 === 0.3;  // false
0.1 + 0.2;          // 0.30000000000000004

Why: 0.1 and 0.2 cannot be represented exactly in binary floating-point — they're infinite repeating binary fractions, just like 1/3 is infinite repeating in decimal. The closest representable values are slightly off, and the error compounds when you add them.

This is not a JavaScript bug. It's a property of IEEE 754. Any language using floats has it.

How to compare floats #

Use a small tolerance, called epsilon:

function nearlyEqual(a, b, eps = Number.EPSILON) {
  return Math.abs(a - b) < eps;
}
nearlyEqual(0.1 + 0.2, 0.3);  // true

Number.EPSILON (about 2.22e-16) is the smallest difference between two adjacent floats around 1.0. For larger numbers, scale the epsilon proportionally.

How to handle money #

Don't use floats. Three common approaches:

  1. Store integers in cents (or smallest unit). 19.99 becomes 1999. Add, subtract, multiply by ratios in integer space. Format for display only.
  2. Use a librarydecimal.js, big.js, dinero.js provide arbitrary-precision decimal arithmetic.
  3. Use BigInt for very large integers — but BigInts can't represent decimals; you still need to encode units yourself.

For any code that calculates money, taxes, or anything where rounding can compound, never use plain number.

The safe-integer range #

Integers above 2⁵³ can no longer be uniquely represented:

Number.MAX_SAFE_INTEGER;       // 9007199254740991 = 2^53 - 1
Number.MAX_SAFE_INTEGER + 1;   // 9007199254740992
Number.MAX_SAFE_INTEGER + 2;   // 9007199254740992 — same as +1! Lost precision.

Number.isSafeInteger(9007199254740991);  // true
Number.isSafeInteger(9007199254740993);  // false

Why: after 2⁵³, the gap between adjacent representable floats becomes larger than 1. Some integers in that range can't be stored exactly.

This matters when:

  • Working with IDs from large-scale databases (Twitter snowflake IDs, ledger IDs)
  • Counting nanoseconds since epoch
  • Cryptographic operations
  • Anything that crosses the 2⁵³ threshold

The fix: BigInt.

BigInt #

A separate numeric type for arbitrary-precision integers. Introduced in ES2020.

const huge = 9007199254740993n;        // literal — note the n suffix
const alsoHuge = BigInt('9007199254740993');

huge + 1n;        // 9007199254740994n — exact
2n ** 64n;         // 18446744073709551616n — way beyond safe-integer range

BigInt rules:

  • n suffix makes a BigInt literal.
  • You cannot mix BigInt and number: 1n + 1 throws TypeError.
  • Convert explicitly: Number(huge) (lossy past 2⁵³), BigInt(n) (only for integer-shaped numbers).
  • No decimals: BigInt is integer-only. 3n / 2n is 1n (truncated), not 1.5n.
  • Slower than regular numbers — uses arbitrary-precision arithmetic. Don't use for hot-path loops.

Use BigInt when:

  • IDs or counters might exceed 2⁵³ – 1
  • Cryptographic math (RSA, large primes)
  • Precise computation where overflow is unacceptable

Special numeric values #

NaN          // Not a Number — result of e.g. 0/0, Math.sqrt(-1), Number('abc')
Infinity     // overflow or 1/0
-Infinity
Number.MAX_VALUE     // ~1.8e308 — largest finite number
Number.MIN_VALUE     // ~5e-324 — smallest positive (not most negative)

NaN is uniquely weird #

It's the only value not equal to itself:

NaN === NaN;             // false
Number.isNaN(NaN);       // true — always use this
Object.is(NaN, NaN);     // true — also works

Number.isNaN('abc');     // false — only true for actual NaN values
isNaN('abc');             // true — global isNaN coerces first; avoid it

Use Number.isNaN(x) to check for NaN. The global isNaN is the older version that coerces — it returns true for any non-numeric value, which is rarely what you want.

Infinity #

1 / 0;                  // Infinity
-1 / 0;                  // -Infinity
Math.log(0);              // -Infinity

Number.isFinite(Infinity); // false
Number.isFinite(42);       // true

Useful for representing "unbounded" in algorithms (initial min/max values, default upper bounds).

The Math API #

Math is a built-in namespace, not a constructor:

Math.PI;                  // 3.141592653589793
Math.E;                   // 2.718281828459045

Math.abs(-5);              // 5
Math.sign(-3);             // -1
Math.floor(3.7);           // 3
Math.ceil(3.1);            // 4
Math.round(3.5);           // 4 — banker's rounding NOT used
Math.trunc(3.7);           // 3 — drops decimal part

Math.sqrt(16);             // 4
Math.cbrt(27);             // 3
Math.pow(2, 10);           // 1024 — also 2 ** 10 (modern)
Math.hypot(3, 4);          // 5

Math.log(Math.E);          // 1 — natural log
Math.log2(8);              // 3
Math.log10(1000);          // 3

Math.min(1, 2, 3);         // 1
Math.max(1, 2, 3);         // 3
Math.min(...arr);          // pass an array via spread

Math.random();             // [0, 1)

Random integers in a range #

function randInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}
randInt(1, 6);  // dice roll

Note: Math.random() is not cryptographically secure. For tokens, passwords, or anything sensitive, use crypto.getRandomValues(new Uint32Array(1))[0] instead.

Number methods #

Formatting #

(3.14159).toFixed(2);        // '3.14' — note: returns a STRING
(3.14159).toPrecision(4);    // '3.142' — also a string
(1234567).toLocaleString();  // '1,234,567' — locale-aware separators
(0.5).toLocaleString('de-DE'); // '0,5' — comma decimal

toLocaleString is the easiest way to format numbers for display. For more control, use Intl.NumberFormat (Lesson 8.3).

Parsing #

Number('42');                // 42
Number('42px');              // NaN — strict
parseInt('42px', 10);        // 42 — stops at first non-digit
parseInt('0xff', 16);        // 255
parseFloat('3.14meters');     // 3.14

Always pass the radix to parseInt. Without it, the behavior depends on the input format and is a footgun.

Number checks #

Number.isInteger(42);             // true
Number.isInteger(42.0);           // true — still an integer
Number.isInteger(42.5);           // false
Number.isFinite(42);              // true
Number.isFinite(Infinity);         // false
Number.isSafeInteger(2 ** 53);    // false

Number.EPSILON;                    // 2.220446049250313e-16
Number.MAX_SAFE_INTEGER;            // 9007199254740991
Number.MIN_SAFE_INTEGER;            // -9007199254740991

Use the Number.xxx variants over the global isNaN / isFinite — they don't coerce.

Common patterns #

Round to N decimals #

function round(n, decimals = 0) {
  const factor = 10 ** decimals;
  return Math.round(n * factor) / factor;
}
round(3.14159, 2);  // 3.14

Not perfectly accurate (still subject to IEEE 754) but good enough for display rounding. For precise money rounding, use a decimal library.

Clamp a value #

const clamp = (n, min, max) => Math.min(Math.max(n, min), max);
clamp(5, 0, 10);   // 5
clamp(-3, 0, 10);  // 0

Sum / average / min / max of an array #

const nums = [3, 7, 1, 9, 4];
const sum = nums.reduce((s, n) => s + n, 0);
const avg = sum / nums.length;
const min = Math.min(...nums);
const max = Math.max(...nums);

For very large arrays, the ...spread approach can blow the call-stack limit. Use a reduce-based min/max instead:

const max = nums.reduce((a, b) => Math.max(a, b), -Infinity);

A summary #

  • number is IEEE 754 double. No separate integer type.
  • 0.1 + 0.2 !== 0.3 — that's the float system, not a bug. Use epsilon comparisons or a decimal library for money.
  • Integers are safe up to 2⁵³ – 1. Past that, use BigInt.
  • BigInt is integer-only and can't mix with number arithmetic.
  • Number.isNaN and Number.isFinite beat their global counterparts (no coercion).
  • Math is a namespace. No new Math(). Direct calls.
  • Math.random is not cryptographically secure. Use crypto.getRandomValues for security.
  • Always pass radix to parseInt.

What's next #

Lesson 8.3 covers dates and IntlDate, Intl.DateTimeFormat, Intl.NumberFormat, Intl.RelativeTimeFormat, and the patterns for internationalized display.

Try it yourself #

The 0.1 + 0.2 puzzle plus the safe-integer one — both in three lines:

YouPredict the output:
console.log(0.1 + 0.2 === 0.3);
console.log(9007199254740993 === 9007199254740992);
console.log(9007199254740993n === 9007199254740992n);
Claude · used js_sandboxOutput:
false — the float result is 0.30000000000000004
true — both integers are past 2^53 and round to the same float
false — BigInt represents them exactly; they’re genuinely different

Two lines, the entire IEEE 754 story. Reach for BigInt when integer precision matters past 2^53; reach for an epsilon comparison (or a decimal library) when float precision matters at all.

Knowing exactly when the precision breaks down is the difference between code that handles money correctly and code that quietly loses a cent here and there.

Up next in JavaScript

More from this topic

View all JavaScript articles →

Enjoyed this article?

Get new JavaScript tutorials delivered. No spam — just code-first articles when they ship.

Leave a Comment

Your email address will not be published. Required fields are marked *