JavaScript Modern

JavaScript BigInt: Working with Large Integers

Master JavaScript BigInt for handling integers beyond Number.MAX_SAFE_INTEGER. Learn syntax, operations, conversions, and practical use cases.

By JavaScript Document Team
bigintes2020numbersdata-typesmodern-javascript

BigInt is a primitive data type in JavaScript that can represent integers with arbitrary precision. Unlike the Number type, which can only safely represent integers up to 2^53 - 1, BigInt can handle much larger integers, limited only by available memory.

Understanding BigInt

BigInt was introduced to solve the problem of JavaScript's Number type limitation when working with large integers, especially important for cryptography, high-precision timestamps, and large ID values.

Creating BigInt Values

// Using the 'n' suffix
const bigInt1 = 123n;
const bigInt2 = 0n;
const bigInt3 = -456n;

// Using the BigInt() function
const bigInt4 = BigInt(789);
const bigInt5 = BigInt('999999999999999999999999999');
const bigInt6 = BigInt('0x1fffffffffffff'); // Hex
const bigInt7 = BigInt('0o777777777777777'); // Octal
const bigInt8 = BigInt('0b1111111111111111'); // Binary

// From Number (within safe range)
const fromNumber = BigInt(123);
console.log(fromNumber); // 123n

// Large numbers that exceed Number.MAX_SAFE_INTEGER
const largeNumber = BigInt('9007199254740993'); // MAX_SAFE_INTEGER + 2
console.log(largeNumber); // 9007199254740993n

// Scientific notation is not allowed with 'n' suffix
// const invalid = 1e10n; // SyntaxError
const valid = BigInt(1e10); // OK: 10000000000n

// Cannot mix decimals with BigInt
// const invalid = 1.5n; // SyntaxError
// const invalid = BigInt(1.5); // RangeError

Number Limitations vs BigInt

// Number.MAX_SAFE_INTEGER
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991 (2^53 - 1)
console.log(Number.MIN_SAFE_INTEGER); // -9007199254740991

// Precision loss with Number
console.log(9007199254740992 === 9007199254740993); // true (!)

// BigInt maintains precision
console.log(9007199254740992n === 9007199254740993n); // false

// Very large numbers
const veryLarge = 123456789012345678901234567890n;
console.log(veryLarge); // 123456789012345678901234567890n

// Operations that would lose precision with Number
const a = 9007199254740991n;
const b = 2n;
console.log(a + b); // 9007199254740993n (correct)

// Same operation with Number loses precision
console.log(9007199254740991 + 2); // 9007199254740992 (precision lost)

BigInt Operations

Arithmetic Operations

// Basic arithmetic
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 (integer division)
console.log(a % b); // 10n (remainder)
console.log(a ** 2n); // 10000n (exponentiation)

// Integer division truncates towards zero
console.log(7n / 3n); // 2n
console.log(-7n / 3n); // -2n
console.log(7n / -3n); // -2n
console.log(-7n / -3n); // 2n

// Unary operations
let x = 10n;
console.log(+x); // TypeError: Cannot convert a BigInt value to a number
console.log(-x); // -10n (negation works)

// Increment and decrement
let counter = 0n;
console.log(++counter); // 1n
console.log(counter++); // 1n
console.log(counter); // 2n
console.log(--counter); // 1n
console.log(counter--); // 1n
console.log(counter); // 0n

// Compound assignment
let value = 100n;
value += 50n; // 150n
value -= 30n; // 120n
value *= 2n; // 240n
value /= 3n; // 80n
value %= 7n; // 3n
value **= 2n; // 9n

Comparison Operations

// Equality comparisons
console.log(10n === 10n); // true
console.log(10n === 10); // false (different types)
console.log(10n == 10); // true (type coercion)

// Relational comparisons
console.log(5n < 10n); // true
console.log(5n < 10); // true (mixed comparison allowed)
console.log(10n > 5); // true
console.log(10n >= 10); // true
console.log(10n <= 10); // true

// Comparing large values
const large1 = BigInt('999999999999999999999999999');
const large2 = BigInt('999999999999999999999999998');
console.log(large1 > large2); // true

// Sorting with BigInts
const mixed = [3n, 1, 2n, 5, 4n];
mixed.sort((a, b) => {
  // Convert to same type for comparison
  if (typeof a === 'bigint' && typeof b === 'bigint') {
    return a < b ? -1 : a > b ? 1 : 0;
  }
  return Number(a) - Number(b);
});
console.log(mixed); // [1, 2n, 3n, 4n, 5]

// Zero comparisons
console.log(0n === -0n); // true
console.log(Object.is(0n, -0n)); // true

Bitwise Operations

// Bitwise AND
console.log(5n & 3n); // 1n (0101 & 0011 = 0001)

// Bitwise OR
console.log(5n | 3n); // 7n (0101 | 0011 = 0111)

// Bitwise XOR
console.log(5n ^ 3n); // 6n (0101 ^ 0011 = 0110)

// Bitwise NOT
console.log(~5n); // -6n (inverts all bits)

// Left shift
console.log(5n << 2n); // 20n (0101 << 2 = 10100)

// Right shift
console.log(20n >> 2n); // 5n (10100 >> 2 = 0101)

// Sign-propagating right shift
console.log(-20n >> 2n); // -5n

// Working with specific bits
function getBit(bigint, position) {
  return (bigint >> position) & 1n;
}

function setBit(bigint, position) {
  return bigint | (1n << position);
}

function clearBit(bigint, position) {
  return bigint & ~(1n << position);
}

function toggleBit(bigint, position) {
  return bigint ^ (1n << position);
}

let num = 0b1010n; // 10n
console.log(getBit(num, 1n)); // 1n
console.log(setBit(num, 0n)); // 11n (0b1011)
console.log(clearBit(num, 1n)); // 8n  (0b1000)
console.log(toggleBit(num, 2n)); // 14n (0b1110)

Type Conversion

Converting Between BigInt and Number

// BigInt to Number
const bigIntValue = 123n;
const numberValue = Number(bigIntValue);
console.log(numberValue); // 123

// Precision loss warning
const largeBigInt = BigInt(Number.MAX_SAFE_INTEGER) + 2n;
const lostPrecision = Number(largeBigInt);
console.log(largeBigInt.toString()); // "9007199254740993"
console.log(lostPrecision); // 9007199254740992 (precision lost!)

// Safe conversion with range check
function safeToNumber(bigint) {
  if (bigint > Number.MAX_SAFE_INTEGER || bigint < Number.MIN_SAFE_INTEGER) {
    throw new RangeError('BigInt value exceeds safe integer range');
  }
  return Number(bigint);
}

// Number to BigInt
const num = 456;
const bigInt = BigInt(num);
console.log(bigInt); // 456n

// String conversions
const fromString = BigInt('789');
const toString = String(789n);
console.log(fromString); // 789n
console.log(toString); // "789"

// Different radix
const hex = 0xffn;
console.log(hex.toString(16)); // "ff"
console.log(hex.toString(10)); // "255"
console.log(hex.toString(2)); // "11111111"

// Parsing with radix
console.log(BigInt('0xFF')); // 255n
console.log(BigInt('ff', 16)); // Error - BigInt() doesn't accept radix
console.log(BigInt('0x' + 'ff')); // 255n

JSON Serialization

// BigInt doesn't serialize to JSON by default
const data = {
  id: 123n,
  count: 456n,
};

// This throws: TypeError: Do not know how to serialize a BigInt
// JSON.stringify(data);

// Solution 1: Convert to string
JSON.stringify(data, (key, value) =>
  typeof value === 'bigint' ? value.toString() : value
);
// '{"id":"123","count":"456"}'

// Solution 2: Custom serialization
BigInt.prototype.toJSON = function () {
  return this.toString();
};

// Now this works
JSON.stringify({ value: 123n }); // '{"value":"123"}'

// Better approach: Custom replacer and reviver
function replacer(key, value) {
  if (typeof value === 'bigint') {
    return {
      type: 'BigInt',
      value: value.toString(),
    };
  }
  return value;
}

function reviver(key, value) {
  if (value && value.type === 'BigInt') {
    return BigInt(value.value);
  }
  return value;
}

const original = { id: 123n, name: 'Test' };
const json = JSON.stringify(original, replacer);
const parsed = JSON.parse(json, reviver);
console.log(parsed); // { id: 123n, name: 'Test' }

Practical Use Cases

Working with Large IDs

// Database IDs that exceed JavaScript's safe integer range
class Database {
  constructor() {
    this.records = new Map();
    this.nextId = 9007199254740992n; // Start beyond MAX_SAFE_INTEGER
  }

  insert(data) {
    const id = this.nextId++;
    this.records.set(id, { id, ...data });
    return id;
  }

  get(id) {
    return this.records.get(BigInt(id));
  }

  update(id, data) {
    const record = this.records.get(BigInt(id));
    if (record) {
      this.records.set(BigInt(id), { ...record, ...data });
      return true;
    }
    return false;
  }

  delete(id) {
    return this.records.delete(BigInt(id));
  }
}

const db = new Database();
const userId = db.insert({ name: 'John', email: 'john@example.com' });
console.log(userId.toString()); // "9007199254740992"

// Twitter Snowflake ID example
function parseSnowflakeId(id) {
  const snowflake = BigInt(id);

  // Extract components (Twitter epoch: 1288834974657)
  const timestamp = (snowflake >> 22n) + 1288834974657n;
  const workerId = (snowflake & 0x3e0000n) >> 17n;
  const processId = (snowflake & 0x1f000n) >> 12n;
  const increment = snowflake & 0xfffn;

  return {
    timestamp: new Date(Number(timestamp)),
    workerId: Number(workerId),
    processId: Number(processId),
    increment: Number(increment),
  };
}

High-Precision Timestamps

// Nanosecond precision timestamps
class HighPrecisionTimer {
  constructor() {
    this.startTime = process.hrtime.bigint();
  }

  elapsed() {
    return process.hrtime.bigint() - this.startTime;
  }

  elapsedMilliseconds() {
    return Number(this.elapsed() / 1000000n);
  }

  elapsedMicroseconds() {
    return Number(this.elapsed() / 1000n);
  }

  elapsedNanoseconds() {
    return this.elapsed();
  }
}

// Usage
const timer = new HighPrecisionTimer();
// Do some work...
console.log(`Elapsed: ${timer.elapsedNanoseconds()}ns`);

// Benchmark with nanosecond precision
function benchmark(fn, iterations = 1000000) {
  const times = [];

  for (let i = 0; i < iterations; i++) {
    const start = process.hrtime.bigint();
    fn();
    const end = process.hrtime.bigint();
    times.push(end - start);
  }

  // Calculate statistics
  const sorted = times.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
  const median = sorted[Math.floor(iterations / 2)];
  const sum = times.reduce((acc, time) => acc + time, 0n);
  const average = sum / BigInt(iterations);

  return {
    median: median.toString() + 'ns',
    average: average.toString() + 'ns',
    min: sorted[0].toString() + 'ns',
    max: sorted[sorted.length - 1].toString() + 'ns',
  };
}

Cryptographic Operations

// Simple modular arithmetic for cryptography
function modExp(base, exponent, modulus) {
  if (modulus === 1n) return 0n;

  let result = 1n;
  base = base % modulus;

  while (exponent > 0n) {
    if (exponent % 2n === 1n) {
      result = (result * base) % modulus;
    }
    exponent = exponent / 2n;
    base = (base * base) % modulus;
  }

  return result;
}

// Example: RSA-like calculation
const p = 61n;
const q = 53n;
const n = p * q; // 3233n
const phi = (p - 1n) * (q - 1n); // 3120n
const e = 17n; // Public exponent

// Encrypt
const message = 123n;
const encrypted = modExp(message, e, n);
console.log(`Encrypted: ${encrypted}`); // 855n

// Extended Euclidean Algorithm
function extendedGCD(a, b) {
  if (a === 0n) {
    return { gcd: b, x: 0n, y: 1n };
  }

  const { gcd, x: x1, y: y1 } = extendedGCD(b % a, a);
  const x = y1 - (b / a) * x1;
  const y = x1;

  return { gcd, x, y };
}

// Modular inverse
function modInverse(a, m) {
  const { gcd, x } = extendedGCD(a, m);
  if (gcd !== 1n) {
    throw new Error('Modular inverse does not exist');
  }
  return ((x % m) + m) % m;
}

Financial Calculations

// Handling money with precision (using smallest unit)
class Money {
  constructor(amount, currency = 'USD') {
    // Store as smallest unit (cents for USD)
    this.cents = BigInt(Math.round(amount * 100));
    this.currency = currency;
  }

  static fromCents(cents) {
    const money = new Money(0);
    money.cents = BigInt(cents);
    return money;
  }

  add(other) {
    if (this.currency !== other.currency) {
      throw new Error('Cannot add different currencies');
    }
    return Money.fromCents(this.cents + other.cents);
  }

  subtract(other) {
    if (this.currency !== other.currency) {
      throw new Error('Cannot subtract different currencies');
    }
    return Money.fromCents(this.cents - other.cents);
  }

  multiply(factor) {
    const bigFactor = BigInt(Math.round(factor * 100));
    return Money.fromCents((this.cents * bigFactor) / 100n);
  }

  divide(divisor) {
    const bigDivisor = BigInt(Math.round(divisor * 100));
    return Money.fromCents((this.cents * 100n) / bigDivisor);
  }

  toString() {
    const dollars = this.cents / 100n;
    const cents = this.cents % 100n;
    const sign = this.cents < 0n ? '-' : '';
    return `${sign}$${abs(dollars)}.${abs(cents).toString().padStart(2, '0')}`;
  }

  toNumber() {
    return Number(this.cents) / 100;
  }
}

function abs(bigint) {
  return bigint < 0n ? -bigint : bigint;
}

// Usage
const price = new Money(19.99);
const tax = price.multiply(0.08);
const total = price.add(tax);
console.log(total.toString()); // $21.59

// Large financial calculations
const revenue = new Money(1234567890.12);
const expenses = new Money(987654321.98);
const profit = revenue.subtract(expenses);
console.log(profit.toString()); // $246913568.14

Working with Binary Data

// 64-bit integer operations
class Int64 {
  constructor(high, low) {
    this.value = (BigInt(high) << 32n) | BigInt(low >>> 0);
  }

  static fromBigInt(bigint) {
    const int64 = new Int64(0, 0);
    int64.value = bigint & 0xffffffffffffffffn;
    return int64;
  }

  toHigh() {
    return Number(this.value >> 32n);
  }

  toLow() {
    return Number(this.value & 0xffffffffn);
  }

  toBuffer() {
    const buffer = new ArrayBuffer(8);
    const view = new DataView(buffer);
    view.setUint32(0, this.toHigh(), false); // Big-endian
    view.setUint32(4, this.toLow(), false);
    return buffer;
  }

  static fromBuffer(buffer) {
    const view = new DataView(buffer);
    const high = view.getUint32(0, false);
    const low = view.getUint32(4, false);
    return new Int64(high, low);
  }
}

// Bit manipulation utilities
function countBits(n) {
  let count = 0n;
  while (n) {
    count += n & 1n;
    n >>= 1n;
  }
  return count;
}

function reverseBits(n, bitWidth = 64n) {
  let result = 0n;
  for (let i = 0n; i < bitWidth; i++) {
    result = (result << 1n) | ((n >> i) & 1n);
  }
  return result;
}

// Bitfield operations
class BitField {
  constructor(size) {
    this.size = BigInt(size);
    this.bits = 0n;
  }

  set(index) {
    if (index >= this.size) throw new Error('Index out of range');
    this.bits |= 1n << BigInt(index);
  }

  clear(index) {
    if (index >= this.size) throw new Error('Index out of range');
    this.bits &= ~(1n << BigInt(index));
  }

  toggle(index) {
    if (index >= this.size) throw new Error('Index out of range');
    this.bits ^= 1n << BigInt(index);
  }

  get(index) {
    if (index >= this.size) throw new Error('Index out of range');
    return Boolean((this.bits >> BigInt(index)) & 1n);
  }

  toString() {
    return this.bits.toString(2).padStart(Number(this.size), '0');
  }
}

Common Pitfalls and Solutions

Mixed Type Operations

// Cannot mix BigInt and Number in arithmetic
// console.log(10n + 5); // TypeError

// Solutions:
// 1. Convert Number to BigInt
console.log(10n + BigInt(5)); // 15n

// 2. Convert BigInt to Number (if safe)
console.log(Number(10n) + 5); // 15

// Helper function for mixed operations
function addMixed(a, b) {
  if (typeof a === 'bigint' || typeof b === 'bigint') {
    return BigInt(a) + BigInt(b);
  }
  return a + b;
}

// Math object doesn't work with BigInt
// Math.sqrt(16n); // TypeError

// Implement BigInt versions
function bigIntSqrt(n) {
  if (n < 0n) throw new Error('Square root of negative number');
  if (n === 0n) return 0n;

  let x = n;
  let y = (x + 1n) / 2n;

  while (y < x) {
    x = y;
    y = (x + n / x) / 2n;
  }

  return x;
}

console.log(bigIntSqrt(16n)); // 4n
console.log(bigIntSqrt(17n)); // 4n (integer result)

Type Checking

// Type checking
console.log(typeof 123n); // "bigint"
console.log(123n instanceof BigInt); // false (BigInt is not a constructor)

// Proper type checking
function isBigInt(value) {
  return typeof value === 'bigint';
}

// Type guards
function ensureBigInt(value) {
  if (typeof value === 'bigint') return value;
  if (typeof value === 'number') return BigInt(value);
  if (typeof value === 'string') return BigInt(value);
  throw new TypeError('Cannot convert to BigInt');
}

// Generic numeric operations
function isNumeric(value) {
  return typeof value === 'number' || typeof value === 'bigint';
}

function max(...values) {
  if (values.length === 0) throw new Error('No values provided');

  return values.reduce((max, val) => {
    if (typeof max === 'bigint' || typeof val === 'bigint') {
      return BigInt(max) > BigInt(val) ? max : val;
    }
    return max > val ? max : val;
  });
}

Performance Considerations

// BigInt operations are generally slower than Number operations
const iterations = 1000000;

// Number performance
console.time('Number');
let numSum = 0;
for (let i = 0; i < iterations; i++) {
  numSum += i;
}
console.timeEnd('Number');

// BigInt performance
console.time('BigInt');
let bigSum = 0n;
for (let i = 0n; i < BigInt(iterations); i++) {
  bigSum += i;
}
console.timeEnd('BigInt');

// Use BigInt only when necessary
class OptimizedCounter {
  constructor() {
    this.count = 0;
    this.bigCount = null;
  }

  increment() {
    if (this.count < Number.MAX_SAFE_INTEGER) {
      this.count++;
    } else {
      if (this.bigCount === null) {
        this.bigCount = BigInt(this.count);
      }
      this.bigCount++;
    }
  }

  getValue() {
    return this.bigCount !== null ? this.bigCount : this.count;
  }
}

Best Practices

  1. Use BigInt only when necessary
// Good: Use Number for regular integers
const age = 25;
const count = 1000;

// Good: Use BigInt for large integers
const largeId = 9007199254740993n;
const timestamp = 1634567890123456789n;
  1. Be explicit about type conversions
// Clear conversion intent
const bigIntValue = BigInt(numberValue);
const numberValue = Number(bigIntValue);

// Check safety when converting
if (bigIntValue <= Number.MAX_SAFE_INTEGER) {
  const safe = Number(bigIntValue);
}
  1. Handle JSON serialization properly
// Consistent serialization strategy
const jsonHelper = {
  stringify: (obj) =>
    JSON.stringify(obj, (k, v) =>
      typeof v === 'bigint' ? v.toString() + 'n' : v
    ),
  parse: (str) =>
    JSON.parse(str, (k, v) =>
      typeof v === 'string' && /^\d+n$/.test(v) ? BigInt(v.slice(0, -1)) : v
    ),
};
  1. Create type-safe utilities
// Type-safe arithmetic utilities
const SafeMath = {
  add: (a, b) => {
    const usesBigInt = typeof a === 'bigint' || typeof b === 'bigint';
    return usesBigInt ? BigInt(a) + BigInt(b) : a + b;
  },

  multiply: (a, b) => {
    const usesBigInt = typeof a === 'bigint' || typeof b === 'bigint';
    return usesBigInt ? BigInt(a) * BigInt(b) : a * b;
  },

  max: (...values) => {
    const usesBigInt = values.some((v) => typeof v === 'bigint');
    if (usesBigInt) {
      return values.reduce((max, val) =>
        BigInt(val) > BigInt(max) ? val : max
      );
    }
    return Math.max(...values);
  },
};

Conclusion

BigInt fills an important gap in JavaScript's numeric capabilities, enabling work with integers beyond the safe integer limit. While it comes with some performance overhead and requires careful handling when mixing with regular numbers, it's invaluable for applications dealing with large IDs, cryptography, high-precision timestamps, and financial calculations. Understanding when and how to use BigInt effectively allows you to handle scenarios that were previously difficult or impossible in JavaScript.