BigInt

Regular JavaScript numbers can only hold integers safely up to a point. That ceiling is 2**53 - 1, which you can read off as Number.MAX_SAFE_INTEGER (9007199254740991). Push past it and the arithmetic quietly goes wrong, because a number is a 64-bit float and simply runs out of precision for large whole values.

alert(9007199254740991 + 1); // 9007199254740992
alert(9007199254740991 + 2); // 9007199254740992  ← should be ...993

BigInt is the fix. It’s a separate numeric type that stores integers of arbitrary length. No precision ceiling, no silent rounding on the digits themselves.

number (64-bit float)
exact integers up to
9007199254740991
beyond that: rounding
bigint
exact integers of any size
1234…7890n
limited only by memory
A Number stays exact only up to 2**53 − 1. BigInt keeps going with no such limit.

Creating a bigint

There are two ways. Add the letter n to the end of an integer literal, or call BigInt() with a string, a number, or another value it can interpret.

// literal: the trailing n makes it a bigint
const bigint = 8402591736482910573648291057364829105n;

// from a string — good when the value comes from user input or JSON
const sameBigint = BigInt("8402591736482910573648291057364829105");

// from a number
const bigintFromNumber = BigInt(10); // same as 10n

The string form matters in practice. A giant number literal in your source is fine, but data arriving at runtime is usually text (JSON has no bigint type), so you’ll often build bigints from strings.

literal 10n
BigInt(“10”)
bigint
10n
Two roads to the same bigint value.

Math operators

For the most part, a bigint acts like a number in arithmetic.

alert(6n + 4n); // 10

alert(7n / 2n); // 3

alert prints a bigint without the trailing n, so you see 10 and 3 — but the underlying values are 10n and 3n. Notice 7n / 2n gives 3n, not 3.5. Bigints are integers only, so division discards the fractional part and rounds toward zero. The result of any operation on bigints is another bigint.

Mixing is forbidden

You cannot combine a bigint and a regular number in the same operation:

alert(6n + 4); // TypeError: Cannot mix BigInt and other types

This is deliberate. A silent conversion could lose information in either direction, so the language refuses to guess and throws instead. When you need to combine them, convert explicitly with BigInt() or Number():

let bigint = 6n;
let number = 4;

// number → bigint, then add (result is 10n, shown as 10)
alert(bigint + BigInt(number)); // 10

// bigint → number, then add (result is a plain number 10)
alert(Number(bigint) + number); // 10
6n bigint
+
4 number
→ TypeError
6n + BigInt(4)
→ 10n
Number(6n) + 4
→ 10
6n + 4 has no defined result type, so it throws. Convert one side first.

These conversions never throw — they’re always silent. That’s convenient, but watch the Number() direction: if the bigint is larger than a number can represent exactly, the excess precision is thrown away. Converting a huge bigint back to a number can hand you a rounded, wrong value with no warning.

let huge = 9007199254740993n;      // exact
alert(Number(huge));               // 9007199254740992  ← precision lost

Comparisons

Relational comparisons — <, >, <=, >= — happily work across the two types:

alert( 2n > 1n ); // true

alert( 2n > 1 ); // true  (comparing a bigint with a number is fine)

Equality is where the type boundary shows up. A bigint and a number can be loosely equal (==) when they represent the same value, but never strictly equal (===), because === also requires the types to match:

alert( 1 == 1n ); // true   — same value, loose check ignores type
alert( 1 === 1n ); // false — different types (number vs bigint)
1 == 1n
value 1 = value 1 →
true
1 === 1n
number ≠ bigint →
false
== compares values across types; === also demands the same type.

Boolean context

Inside an if, or with logical operators, a bigint follows the same truthy/falsy rules as a number. Only 0n is falsy; every other bigint is truthy.

if (0n) {
  // never runs — 0n is falsy
}

The logical operators ||, &&, and ! behave just as they do with numbers, returning one of their operands rather than a plain boolean:

alert( 7n || 3 ); // 7  (returns 7n, which is truthy, so || stops there)

alert( 0n || 3 ); // 3  (0n is falsy, so || moves on to 3)

Polyfills

Supporting bigints in engines that lack them is genuinely hard. The trouble is that operators like +, -, and / mean different things depending on whether their operands are numbers or bigints — recall that / truncates for bigints but not for numbers. A polyfill can’t just add a function; it would have to inspect your code and rewrite every operator into a call that checks types at runtime. That’s clumsy and slow, which is why no solid drop-in polyfill exists.

The JSBI library takes the opposite approach. Instead of making native syntax work everywhere, it asks you to write the arithmetic as method calls:

Operation native BigInt JSBI
Creation from Number a = BigInt(250) a = JSBI.BigInt(250)
Addition c = a + b c = JSBI.add(a, b)
Subtraction c = a - b c = JSBI.subtract(a, b)

JSBI emulates big numbers internally while following the spec closely, so the results match native bigints. Then a Babel plugin rewrites those JSBI.add(...) calls back into native a + b for engines that do support bigints.

your source
JSBI.add(a, b)
Babel →
modern engine a + b
as-is →
old engine JSBI emulation
Write once in JSBI; ship native where possible, emulated where not.

You write your code in JSBI once. Old engines run the emulation directly; modern ones get the plugin’s native rewrite. The same source is “bigint-ready” either way.

References