JAVASCRIPT / NUMBERS, MATH, AND BIGINT
BigInt for integers beyond the safe range
Use BigInt literals and conversions to work with integers past 2**53 exactly, and know the rules that stop BigInt from mixing with Number.
What you will learn
- Write big integers with a trailing n, or convert exact digit strings with BigInt(str)
- Mixed BigInt/Number arithmetic throws; == compares values but === compares types too
- Expect truncation from BigInt division and no support from the Math namespace
- Model fixed-width 64-bit integers with shifts plus BigInt.asUintN and asIntN
Understanding BigInt for integers beyond the safe range
A BigInt is a separate primitive whose values are plain mathematical integers with no fixed width; the only limit is how much memory the engine will hand out. You create one by suffixing digits with n, as in 9007199254740993n, or by calling BigInt on a digit string or an integral Number, and typeof returns 'bigint'. Because the value is stored as a sequence of digits rather than as the sign, exponent, and 53-bit significand of a double, adding one to 2**53 actually changes the value instead of rounding back to where it started.
The strict separation from Number is deliberate, not an oversight. Mixed arithmetic would have to pick a result type and both choices lose information: widening the BigInt into a double rounds it, and narrowing the Number into a BigInt discards any fraction, so +, -, *, /, % and ** throw a TypeError on mixed operands (V8 words it "Cannot mix BigInt and other types, use explicit conversions"). Comparisons are exempt because they only produce a boolean and can compare exact values, which is why 10n == 10 and 10n < 11 are true while 10n === 10 is false. Unary + is the odd exception: it throws even on a lone BigInt, because asm.js depends on +x always yielding a Number.
BigInt is an integer type all the way down, and that shapes the rest of its behaviour: division truncates toward zero, Math functions reject it, JSON.stringify throws on it, and there is no >>> operator because there is no fixed width to shift bits into. Reach for it when a value must stay exact — 64-bit database or snowflake IDs, hashes, nanosecond timestamps, bit fields — and convert to a string at the edges where you serialize or display. Every operation allocates, so BigInt is measurably slower than double arithmetic and a poor default for ordinary counting.
const big = 9007199254740993n; // 2n ** 53n + 1n
console.log(String(big));
console.log(Number(big));
console.log(String(big * 3n));
console.log(String(7n / 2n));
console.log(typeof big, typeof 9007199254740993);
try {
big + 1;
} catch (err) {
console.log(err.name);
}BigInt is a distinct primitive holding exact integers of unbounded size, and its refusal to mix with Number is what preserves that exactness.
Worked examples
Comparing across the two types
Shows which operators are allowed to mix BigInt with Number and what they actually compare.
console.log(10n == 10, 10n === 10);
console.log((2n ** 53n + 1n) == 9007199254740992);
console.log(10n < 11, 0n ? 'truthy' : 'falsy');
console.log([3n, 1, 2n].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join(','));Example explained
Line 110n == 10 is true because loose equality compares exact mathematical values; === is false because typeof differs.
Line 2(2n ** 53n + 1n) == 9007199254740992 is false, which proves == does not round the BigInt into a double first.
Line 3Relational operators may mix types since they only return a boolean, and 0n is the only falsy BigInt.
Line 4The comparator uses < and > rather than a - b, because subtracting a Number from a BigInt would throw.
Keeping a 64-bit ID intact
Demonstrates that parsing a large ID as a Number corrupts it before BigInt can help.
const raw = '9223372036854775807'; // largest signed 64-bit integer
const viaNumber = Number(raw);
const viaBigInt = BigInt(raw);
console.log(viaNumber);
console.log(String(viaBigInt));
console.log(viaNumber % 1000, String(viaBigInt % 1000n));
console.log(String(BigInt(viaNumber)));Example explained
Line 1Number(raw) prints 9223372036854776000 because the nearest double is 2**63, and that decimal is the shortest one that round-trips to it.
Line 2BigInt(raw) reads the digits directly, so the last digit survives.
Line 3The remainders differ, 808 versus 807, which is how a silently rounded ID surfaces as an off-by-a-few bug.
Line 4BigInt(viaNumber) cannot repair anything: it faithfully copies 2**63, the value the double already holds.
Fixed-width bit work with asUintN and asIntN
Packs two 32-bit halves into one 64-bit value and reinterprets the low half signed and unsigned.
const packed = (0x1234abcdn << 32n) | 0xdeadbeefn;
console.log(packed.toString(16));
console.log((packed >> 32n).toString(16));
console.log(BigInt.asUintN(32, packed).toString(16));
console.log(String(BigInt.asIntN(32, packed)), 0xdeadbeef | 0);Example explained
Line 1Shifting a BigInt left by 32n keeps every bit, whereas Number bitwise operators would first truncate the operand to 32 bits.
Line 2packed >> 32n recovers the high half, and the shift count itself must also be a BigInt.
Line 3BigInt.asUintN(32, packed) keeps only the low 32 bits as an unsigned value, giving 0xdeadbeef.
Line 4asIntN(32, packed) reinterprets those same bits as two's complement, matching what 0xdeadbeef | 0 produces for Numbers.
Important notes
JSON.stringify(1n) throws "Do not know how to serialize a BigInt", and JSON.parse can never produce one, so IDs must travel as strings and be converted back explicitly.
The n suffix is needed on every operand, including hex like 0xffn and the shift count in x << 32n; a bare 32 there throws.
Common mistakes
Writing count + 1 when count is a BigInt: mixed arithmetic throws a TypeError at runtime instead of quietly coercing, so a line that looks harmless crashes.
Building a BigInt out of a Number, as in BigInt(Number(id)) or BigInt(parseInt(id)): the rounding already happened, so you get an exact copy of the wrong integer.
Expecting 5n / 2n to be 2.5 or 3n: it truncates to 2n, so averages and percentages come out low with nothing thrown to warn you.
Try it yourself
Change, predict, then run
In a browser console, print Number('18446744073709551615') next to String(BigInt('18446744073709551615')), the largest unsigned 64-bit integer. Then compare Number('18446744073709551615') % 7 with BigInt('18446744073709551615') % 7n and explain which one is the true remainder.
Open the JavaScript workspaceCheck your understanding
Why does 10n == 10 evaluate to true while 10n + 10 throws a TypeError?
- Loose equality compares exact mathematical values and returns only a boolean, while arithmetic would have to choose a result type and either choice can lose information
- == converts the BigInt to a Number first, which always succeeds for values inside the safe range
- == works on primitives, but BigInt arithmetic needs the value wrapped in an object first
- 10n == 10 is actually false; only comparisons between two BigInts can be true
Show answer
Equality and relational operators are specified to compare the exact mathematical values of the two operands, so nothing has to be converted and the result is just true or false. Arithmetic must return a single type, and rounding the BigInt into a double or discarding the Number's fraction would silently corrupt the result, so the spec throws instead. Option 2 is tempting but wrong: if == rounded the BigInt into a double, then (2n ** 53n + 1n) == 9007199254740992 would be true, and it is false.