When Number Isn't Big Enough
JavaScript Numbers are 64-bit floats, and they silently lose precision for integers above 9 quadrillion (2^53). BigInt solves the problem: arbitrarily large integers without rounding errors, for IDs, cryptography, timestamps and financial calculations.
Table of Contents
- 1. The problem: Number loses precision
- 2. What is BigInt and how do you create one?
- 3. Arithmetic operators with BigInt
- 4. Type conversion: mixing BigInt and Number
- 5. BigInt and JSON: solving serialization problems
- 6. Bitwise operations with BigInt
- 7. BigInt in cryptography
- 8. Performance: BigInt vs. Number
- 9. BigInt vs. Number: comparing the limits
- 10. Summary
- 11. FAQ
1. The problem: Number loses precision
JavaScript has only one numeric type: Number. It follows the IEEE 754 double-precision format with 64 bits, 52 of which are the mantissa. That means integers can be represented exactly as long as they stay below 2^53 (9,007,199,254,740,992). This limit is baked into the language as Number.MAX_SAFE_INTEGER. Beyond that limit, rounding kicks in: Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 evaluates to true, because both values get rounded to the nearest representable float.
In practice this problem shows up with database IDs from Twitter, Snowflake or other distributed systems, since those IDs are often 64-bit integers and exceed Number.MAX_SAFE_INTEGER. A Twitter ID like 1591874983876853760 gets rounded when represented as a JavaScript Number and can end up addressing the wrong resource. Nanosecond timestamps, cryptographic keys, blockchain values and certain hash values also exceed the safe limit. BigInt was introduced in ES2020 as the answer to exactly this problem.
2. What is BigInt and how do you create one?
BigInt is a primitive type in JavaScript, introduced in ES2020, that represents arbitrarily large integers with exact precision, limited only by available memory. There are two ways to create a BigInt: the literal suffix n or the BigInt() function. The literal 9007199254740993n is a BigInt, while 9007199254740993 is a Number that gets rounded. BigInt("9007199254740993") converts a string to a BigInt, which is the safe way to go when the number comes from an API.
BigInt is its own primitive type, identifiable via typeof 1n === "bigint". It behaves like an integer: there are no decimal places, no Infinity, no NaN. Division with BigInt is integer division: 5n / 2n === 2n (the remainder is truncated). Negative BigInts work as expected: -5n / 2n === -2n. There are BigInt.asIntN(width, value) and BigInt.asUintN(width, value), which clamp the BigInt to a fixed bit width, useful for simulating 32-bit or 64-bit integers.
// BigInt creation: literal suffix 'n' or BigInt() constructor
const fromLiteral = 9007199254740993n; // exact, no rounding
const fromNumber = BigInt(9007199254740992); // safe conversion from Number
const fromString = BigInt("9007199254740993456789"); // from API response
// typeof distinguishes BigInt from Number
console.log(typeof 42n); // "bigint"
console.log(typeof 42); // "number"
// Demonstrate the Number precision problem
console.log(9007199254740993 === 9007199254740994); // true, Number is wrong!
console.log(9007199254740993n === 9007199254740994n); // false, BigInt is correct
// BigInt division is integer division (truncates fractional part)
console.log(17n / 5n); // 3n, not 3.4
console.log(-17n / 5n); // -3n, truncates toward zero
// Clamping to fixed bit width (useful for simulation)
const u32max = BigInt.asUintN(32, 0xFFFFFFFFn + 1n);
console.log(u32max); // 0n, wraps around like uint32 overflow
3. Arithmetic operators with BigInt
All the basic arithmetic operators work with BigInt: addition (+), subtraction (-), multiplication (*), integer division (/), modulo (%) and exponentiation (**). The comparison operators (<, >, ===, !==) work as well. Important: the unary + operator does not work with BigInt, a deliberate design decision that prevents accidental conversions to Number.
Bitwise operators (&, |, ^, ~, <<, >>) all work with BigInt. The unsigned right shift (>>>) does not exist for BigInt, since BigInt has no fixed bit format and the concept of "unsigned" would be meaningless. Logical operators (&&, ||) and the ternary operator work with BigInt too. 0n is falsy, all other BigInts are truthy, consistent with the other JavaScript types. Loose comparison with == works between BigInt and Number, strict comparison with === does not, since the types differ.
4. Type conversion: mixing BigInt and Number
The biggest stumbling block with BigInt is that JavaScript allows no implicit conversion between BigInt and Number. 1n + 1 throws a TypeError: Cannot mix BigInt and other types. This is a deliberate design decision: implicit conversions would cause silent precision loss and undermine the whole point of BigInt. Explicit conversion is always required: Number(1n) or BigInt(1).
Converting from BigInt to Number loses precision if the value exceeds Number.MAX_SAFE_INTEGER, which is the exact error you're trying to avoid in the first place. So you should always check whether the conversion is safe. BigInt(x) from a Number only works for integer values: BigInt(1.5) throws a RangeError. To compare against a Number you can use loose comparison: 1n == 1 is true, because JavaScript has a coercion rule for == that makes BigInt and Number comparable.
// Explicit conversion required, no implicit mixing
const big = 9007199254740993n;
// Safe: check before converting BigInt → Number
function bigIntToNumber(n) {
if (n > BigInt(Number.MAX_SAFE_INTEGER) || n < BigInt(Number.MIN_SAFE_INTEGER)) {
throw new RangeError(`BigInt ${n} exceeds safe integer range`);
}
return Number(n);
}
// Safe conversion from API string to BigInt
function parseId(idString) {
const parsed = BigInt(idString);
return parsed;
}
// Loose vs strict equality
console.log(1n == 1); // true, loose equality allows type coercion
console.log(1n === 1); // false, strict equality checks type
console.log(1n < 2); // true, comparison operators work across types
// Formatting large BigInt values
const trillion = 1_000_000_000_000n; // numeric separators work with BigInt
console.log(trillion.toString()); // "1000000000000"
console.log(trillion.toString(16)); // "e8d4a51000" (hex representation)
console.log(trillion.toString(2)); // binary representation
5. BigInt and JSON: solving serialization problems
BigInt cannot be serialized directly with JSON.stringify(), it throws a TypeError: Do not know how to serialize a BigInt. This is a deliberate choice, since JSON has no native BigInt type and a silent conversion to Number would once again cost precision. The solution is a custom replacer in JSON.stringify() and a custom reviver in JSON.parse().
A widely used pattern: BigInt values are transmitted in JSON as strings (with a suffix like "n" or wrapped in an object). During parsing, these strings are detected and converted back to BigInt. Alternatively, there's the json-bigint library, which handles correct parsing of large numbers in JSON, particularly useful when an external API transmits large IDs as numbers (not as strings) in JSON, which is allowed by the JSON specification but costs precision in JavaScript.
6. Bitwise operations with BigInt
One of the surprising use cases for BigInt is bitwise operations on values that exceed 32 bits. JavaScript Numbers perform bitwise operations on 32-bit integers: values are internally converted to a 32-bit integer, the operation is performed, and the result is converted back to Number. That means bitwise operations on large numbers with Number lose information beyond the 32-bit boundary.
With BigInt, bitwise operations work on arbitrarily wide values, limited only by memory. This matters for flags and bitmasks that need more than 32 bits, for 64-bit flags from low-level system APIs, for implementations of hash functions that work with 64-bit values, and for protocol implementations that operate on large bit fields. BigInt.asUintN(64, value) can be used to simulate 64-bit integer arithmetic with well-defined overflow behavior.
// Bitwise operations on large values, BigInt handles >32 bits correctly
const flags64 = 0b1000000000000000000000000000000000000000000000000000000000000001n;
// Set, check, clear bits
const BIT_ADMIN = 1n << 0n;
const BIT_WRITE = 1n << 1n;
const BIT_EXECUTE = 1n << 63n; // 64th bit, impossible with Number
let permissions = 0n;
permissions |= BIT_ADMIN; // set admin flag
permissions |= BIT_EXECUTE; // set execute flag
const isAdmin = (permissions & BIT_ADMIN) !== 0n; // true
const isExecute = (permissions & BIT_EXECUTE) !== 0n; // true
const isWrite = (permissions & BIT_WRITE) !== 0n; // false
// Simulate 64-bit unsigned arithmetic with defined overflow
function uint64Add(a, b) {
return BigInt.asUintN(64, a + b);
}
console.log(uint64Add(0xFFFFFFFFFFFFFFFFn, 1n)); // 0n, wraps to 0 like uint64
// FNV-1a hash (64-bit variant) implemented in BigInt
function fnv1a64(str) {
const PRIME = 1099511628211n;
const OFFSET = 14695981039346656037n;
let hash = OFFSET;
for (const char of str) {
hash ^= BigInt(char.charCodeAt(0));
hash = BigInt.asUintN(64, hash * PRIME);
}
return hash;
}
7. BigInt in cryptography
Cryptographic algorithms such as RSA, Diffie-Hellman and elliptic curves work with very large integers, typically 2048 to 4096 bits in size. Before BigInt was available in JavaScript, crypto libraries had to bring their own BigInteger implementations (for example jsbn or forge). With native BigInt, these operations can be implemented directly in JavaScript, without external dependencies for the underlying arithmetic.
For production use in cryptography, however, you should prefer the browser's SubtleCrypto API, which offers native, side-channel resistant implementations. BigInt is well suited for understanding and prototyping cryptographic algorithms, as well as for use cases where SubtleCrypto isn't enough, such as implementing Shamir's Secret Sharing, Pedersen commitments, or certain protocols that require specific prime number operations. Modular exponentiation, the core of RSA, can be implemented directly with BigInt using the ** operator and %.
8. Performance: BigInt vs. Number
BigInt arithmetic is slower than Number arithmetic, which is unavoidable because Number maps directly onto IEEE 754 hardware operations, while BigInt has to manage arbitrary sizes and implement software multi-precision arithmetic. In V8 benchmarks (Chrome/Node.js), a simple BigInt addition is roughly 5 to 50 times slower than the equivalent Number operation, depending on the size of the operands and whether the JIT compiler was able to optimize the code.
That means BigInt is not meant for applications that perform intensive numerical computations with many iterations. For physics simulations, game engines and signal processing, Number (or TypedArray) remains the right choice. BigInt is for precision, not for speed. In scenarios where correct results for large integers are required and the number of operations is manageable, such as ID processing, protocol implementation, or occasional hash calculations, the performance difference is irrelevant.
9. BigInt vs. Number: comparing the limits
The choice between BigInt and Number is not a matter of personal preference, but a factual decision based on requirements. If the integers you need to process are guaranteed to stay below 2^53, Number is correct and significantly more performant. If not, or if you can't be sure, BigInt is the safe choice.
| Property | Number | BigInt | Recommendation |
|---|---|---|---|
| Maximum safe integer | 2^53 − 1 | Unlimited | BigInt for IDs >2^53 |
| Decimal places | Yes (float) | No (integer only) | Number for floating point |
| JSON serialization | Direct | Manual (replacer) | Transmit BigInt as a string |
| Performance | Very fast (hardware) | 5 to 50x slower | Number for bulk operations |
| Bitwise operations >32 bit | Loses bits | Exact | BigInt for 64-bit flags |
BigInt and Number complement each other: Number for floating point math, performance-critical calculations and the vast majority of numeric operations. BigInt for correctly representing large integers, 64-bit flags, cryptography prototypes and scenarios where a single rounding error would have serious consequences. The two types cannot be mixed, which feels frustrating at first, but it forces explicit conversions and thereby prevents accidental precision loss.
Mironsoft
JavaScript development, data integrity and safe ID processing
Fixing precision problems with large numbers in your application?
We analyze existing systems for hidden precision loss in ID processing, timestamps and numeric calculations, and migrate to BigInt where necessary.
Precision audit
Analysis for hidden Number precision loss in IDs, timestamps and financial data
BigInt migration
Gradual migration from Number to BigInt including JSON serialization and API adapters
Code review
Review for implicit BigInt/Number mixing and flawed conversion logic
10. Summary
BigInt is JavaScript's answer to the problem, present since ES1, that Number cannot exactly represent arbitrarily large integers. Using the suffix n or the BigInt() function you create BigInt values that support all arithmetic operators and bitwise operators. Implicit conversions between BigInt and Number are forbidden; explicit conversions are always required. For JSON serialization, custom replacers and revivers are necessary. Performance is worse than Number, but that's the deliberate trade-off for exact precision.
The typical use cases are clear: BigInt for Twitter/Snowflake IDs and other 64-bit IDs from external systems, for nanosecond timestamps, for 64-bit flags and bitmasks, for cryptographic calculations, and for any scenario where rounding errors on large integers are unacceptable. Number remains the right choice for floating point calculations, performance-critical loops and the vast majority of numeric operations in JavaScript applications.
BigInt in JavaScript, the essentials at a glance
When to use BigInt
Integers above 2^53, 64-bit IDs from external systems, cryptography, 64-bit flags. Not for floating point or performance-critical bulk operations.
Syntax
Literal: 9007199254740993n, suffix n. From string: BigInt("123456789012345678"). From Number only for integer, safe values.
Conversion
No implicit mixing with Number, TypeError. Explicit: Number(bigint) checks the safety boundary. BigInt(number) only for integer Numbers.
JSON
JSON.stringify throws a TypeError for BigInt. Solution: a replacer that serializes BigInt as a string, a reviver that converts it back.