Introduction
You're querying a database and the ID comes back as 18446744073709551615. You parse it into JavaScript, do some arithmetic, and suddenly your number is corrupted. This is BigInt and large numbers: a hands-on guide for engineers—a practical look at why JavaScript's standard number type fails us at scale, and how to handle it in production systems.
JavaScript's Number type has served us well for years, but it has hard limits. When we work with database IDs, financial transactions, distributed system timestamps, or cryptographic values, we regularly exceed those limits. Understanding when and how to use BigInt is essential for anyone building serious backend systems, APIs, or data-intensive applications.
Safe integer limit: Number.MAX_SAFE_INTEGER is 2^53 - 1
JavaScript's Number type is a 64-bit floating-point value following the IEEE 754 standard. Of those 64 bits, 53 are used for the significand (the actual number), which means integers are guaranteed to be exact only up to 253 - 1, which is 9007199254740991.
This limit exists because JavaScript uses the same type for both integers and decimals. The trade-off made for floating-point arithmetic means we lose precision beyond that boundary. Let's see what happens when we exceed it:
const MAX_SAFE = Number.MAX_SAFE_INTEGER;
console.log(MAX_SAFE); // 9007199254740991
const x = 9007199254740992;
const y = 9007199254740993;
console.log(x === y); // true — they're the same!
console.log(x); // 9007199254740992
console.log(y); // 9007199254740992
This silently breaks equality checks, comparisons, and arithmetic. In production, this might mean:
- Two different database records appearing as the same ID
- Financial calculations with cents or fractions becoming incorrect
- Distributed tracing IDs colliding unexpectedly
- Pagination cursors breaking in large datasets
A common scenario: your PostgreSQL database uses 64-bit integers for IDs, and a client requests record 9223372036854775807 (the maximum signed 64-bit integer). JavaScript receives it, and it becomes 9223372036854775000 due to precision loss. The record fetch fails or returns the wrong data.
BigInt: Arbitrary-precision integers with the n suffix
BigInt is a JavaScript primitive type introduced in ES2020 that represents integers of arbitrary size. Unlike Number, it doesn't have a precision limit. You create a BigInt by appending the letter n to an integer literal, or by wrapping a value in the BigInt() constructor.
// Literals
const big1 = 9007199254740992n;
const big2 = 9223372036854775807n;
// Constructor
const big3 = BigInt('18446744073709551615');
const big4 = BigInt(42); // regular numbers work too
console.log(big1 + big2); // 18230571328595516799n
console.log(big3 - 1n); // 18446744073709551614n
BigInt arithmetic works as you'd expect for integers:
const a = 100n;
const b = 30n;
console.log(a + b); // 130n
console.log(a - b); // 70n
console.log(a * b); // 3000n
console.log(a / b); // 3n — truncates toward zero
console.log(a % b); // 10n
console.log(a ** 2n); // 10000n
Important constraint: you cannot mix BigInt and Number in arithmetic. This is deliberate—it prevents accidental precision loss.
const big = 100n;
const num = 50;
// All throw TypeError: Cannot mix BigInt and other types
console.log(big + num); // ❌
console.log(big === num); // ❌ (but === always returns false, doesn't throw)
console.log(big > num); // ❌
In a real backend scenario, consider an e-commerce system that tracks order IDs from a distributed ledger. The IDs are 256-bit numbers. You'd use BigInt:
interface Order {
id: bigint;
amount: number; // price in cents as regular number
createdAt: Date;
}
const order: Order = {
id: BigInt('340282366920938463463374607431768211455'),
amount: 2999, // $29.99
createdAt: new Date(),
};
// Safe comparison
if (order.id > 1000000000000000000n) {
console.log('Large order ID detected');
}
JSON gotcha: BigInt doesn't serialize to JSON by default
This is where many teams hit their first production bug with BigInt. JSON has no native representation for BigInt, so calling JSON.stringify() on an object containing BigInt throws a TypeError:
const data = {
id: 123456789012345n,
name: 'Widget',
};
console.log(JSON.stringify(data));
// TypeError: Do not know how to serialize a BigInt
You must handle this explicitly. The common approaches are:
Option 1: Custom replacer function
const data = {
id: 123456789012345n,
name: 'Widget',
budget: 50000n,
};
const json = JSON.stringify(data, (key, value) => {
if (typeof value === 'bigint') {
return value.toString();
}
return value;
});
console.log(json);
// {"id":"123456789012345","name":"Widget","budget":"50000"}
Option 2: Convert before serialization
interface OrderResponse {
id: string; // BigInt as string
name: string;
amount: number;
}
function orderToResponse(order: { id: bigint; name: string; amount: number }): OrderResponse {
return {
id: order.id.toString(),
name: order.name,
amount: order.amount,
};
}
const order = {
id: 123456789012345n,
name: 'Widget',
amount: 5000,
};
const response: OrderResponse = orderToResponse(order);
console.log(JSON.stringify(response));
// {"id":"123456789012345","name":"Widget","amount":5000}
Option 3: Custom toJSON method
class Order {
id: bigint;
name: string;
constructor(id: bigint, name: string) {
this.id = id;
this.name = name;
}
toJSON() {
return {
id: this.id.toString(),
name: this.name,
};
}
}
const order = new Order(123456789012345n, 'Widget');
console.log(JSON.stringify(order));
// {"id":"123456789012345","name":"Widget"}
Option 2 (explicit conversion) is most common in modern API architectures because it's type-safe and intention-clear.
IDs as strings: Large database IDs should travel as strings
The practical consensus among teams working at scale is simple: large IDs should always be strings in API contracts, even when they're BigInt in your backend. This solves several problems at once:
- Language compatibility: Not all languages have arbitrary-precision integers. A client in Python, Go, or Java needs the ID as a string to avoid truncation.
- JSON safety: Strings don't have the serialization issues that BigInt does.
- Frontend compatibility: JavaScript in the browser still uses Number for JSON parsing, so large numeric IDs get corrupted. Strings are safe.
- Cache keys and logging: Strings work everywhere without conversion logic.
A real-world API pattern:
// Backend: PostgreSQL returns a numeric ID
interface InternalOrder {
id: bigint;
userId: bigint;
total: number;
}
// API contract: IDs are strings
interface OrderDTO {
id: string;
userId: string;
total: number;
}
function orderToDTO(order: InternalOrder): OrderDTO {
return {
id: order.id.toString(),
userId: order.userId.toString(),
total: order.total,
};
}
// Controller
app.get('/orders/:id', async (req, res) => {
const orderId = BigInt(req.params.id); // Parse from string
const order = await db.orders.findById(orderId);
res.json(orderToDTO(order));
});
This way, clients receive:
{
"id": "9223372036854775807",
"userId": "4611686018427387903",
"total": 2999.99
}
No truncation, no precision loss, compatible everywhere.
Common mistakes
- Mixing BigInt and Number in comparisons:
100n === 100returns false (different types), and arithmetic like100n + 50throws. Always convert one side to match the other before operations. - Forgetting to convert BigInt to string before sending as JSON: This throws at runtime, breaking the API response. Use a custom replacer or convert explicitly in DTOs.
- Using BigInt for IDs everywhere in internal code: It's tempting, but most values don't need it. Reserve BigInt for genuinely large values. Regular numbers work fine for most IDs under 2^53.
- Parsing user input directly into BigInt without validation:
BigInt(userInput)throws if the input isn't a valid integer. Always validate and catch errors. - Storing BigInt in databases designed for strings: If your schema stores IDs as VARCHAR, keep them as strings in your application. Convert only when needed for arithmetic.
Key takeaways
- JavaScript's
Numbertype loses precision beyond 253 - 1. This breaks database IDs, distributed IDs, and financial calculations at scale. BigIntis the solution for arbitrary-precision integers, created with thensuffix or theBigInt()constructor.- BigInt doesn't serialize to JSON. Use a replacer function or convert to strings explicitly before sending to clients or storage.
- In API contracts and JSON payloads, send large IDs as strings. This ensures compatibility across languages, frontends, and platforms.
- Don't mix BigInt and Number in operations. Convert one side to match the other, or structure your types to prevent this at compile time.






Comments
Loading comments…