completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

BigInt inside the engine

Learn how V8 stores BigInts as heap digit arrays, chooses arithmetic algorithms, converts strings, handles 64-bit paths, and enforces size limits.

By the end, you can
  • 01
    Read how V8 stores a BigIntName the sign, the length, and the 64-bit digits, say why the low digit comes first and why zero has no digits, and explain why even 1n is a heap object.
  • 02
    Link each operation to its costFollow a carry from one digit to the next, step through one Karatsuba split, and say which multiplication method V8 picks for a size, and why text conversion takes work.
  • 03
    Choose the right number toolPick between Number, BigInt, BigInt64Array, and 64-bit wrapping, keep Number and BigInt math apart, and avoid promises about speed.

A BigInt is a list of digits in memory

A BigInt is JavaScript's type for whole numbers of any size, with no rounding. You write one with an n at the end, like 10n. This lesson looks at how V8 keeps a BigInt in memory. V8 is the JavaScript engine inside Chrome and Node.js. Every fact here was checked on V8 version 12.4, the version inside Node 22.

V8 keeps each BigInt on the heap. The heap is the part of memory where the engine keeps objects. A stored BigInt has three parts:

  • The sign: is the number negative or not?
  • The length: how many digits the number uses.
  • The digits: the number cut into 64-bit pieces. One 64-bit piece holds any whole number from 0 up to 264 − 1, about 18 quintillion. V8 calls each piece a digit.
Definition

In V8, every BigInt is a heap object of type BIG_INT_BASE_TYPE. It holds a sign, a length, and its 64-bit digits, low digit first. A small Number can be a Smi: a small integer kept right inside the value, with no heap object. BigInt has no such short form. Even 1n is a heap object.

This lesson builds on the beginner BigInt lesson and Tagged values, Smis & heap numbers. It is not about how to write BigInt code. It is about how V8 stores BigInts and what each operation costs:

  • why math on a BigInt always makes a new BigInt,
  • why V8 multiplies in different ways for different sizes,
  • why turning a BigInt into decimal text takes work,
  • and how fixed 64-bit tools, like BigInt64Array, fit in.
Real-life analogyA long number written across boxes on a form

Picture a form where you write a long number across several boxes. One box says plus or minus. You also note how many number boxes you used. Each number box holds one part of the number. To change the number, you fill in a fresh form. You never rub out the old one.

In real life: A plus or minus box
In JavaScript: The sign
In real life: A note of how many number boxes you used
In JavaScript: The length
In real life: Each number box holds one part
In JavaScript: One 64-bit digit
In real life: A changed number goes on a fresh form
In JavaScript: Math on a BigInt gives a new BigInt

Sign, length, and 64-bit digits

The main source is V8's file src/objects/bigint.h. It describes BigIntBase, the shape every BigInt has:

  • The sign and the length share one small field. The sign is 1 for a negative number and 0 for any other number.
  • Each digit has the type digit_t. It is as wide as a memory address on the machine (uintptr_t). On the 64-bit Node 22 build used by the tests, that is 64 bits.
  • The digits come right after, in a list that is as long as the number needs.

So 2n ** 64n, which is one more than the biggest 64-bit digit, needs two digits: 0x0 first, then 0x1. The 0x means the digit is written in hexadecimal (base 16). The low digit comes first. This order is called little-endian.

2n ** 64n inside V8. Four cards in a row: sign 0, which means not negative; length 2, which means two digits; digit 0 is 0x0, the low digit; digit 1 is 0x1, the high digit.2n ** 64nsign: 0length: 20x00x1not negativetwo digitsdigit 0: lowdigit 1: highthe low digit comes first
V8 keeps the sign and the length apart from the digits. Each digit holds 64 bits, and the low digit comes first, so 2n ** 64n is stored as 0x0, then 0x1.

Each row of the table is one part of a stored BigInt. Read across: the part, what V8 shows, and what that means.

The parts of a V8 BigInt
PartWhat V8 showsWhat it means
Shape (V8 calls it a map, not the JavaScript Map)%DebugPrint shows the type BIG_INT_BASE_TYPE. The shape itself sits in read-only memory.Every ordinary BigInt in V8 has this same shape.
Sign and lengthOne small field holds both: 1 bit for the sign and 30 bits for the length.sign: 1 means negative. length: 0 means the number is 0n.
DigitsA list of uintptr_t digits, as wide as a memory address. In Node's 64-bit build, each digit is 64 bits.The low digit comes first: 2n ** 64n prints 0x0, then 0x1.

%DebugPrint is a debug command built into V8. It prints how a value is stored. It only works in Node, and only when you start Node with the --allow-natives-syntax flag. So this code does not run in the browser.

Print the real layout in Node with %DebugPrintJavaScript
// Runs only in Node started with --allow-natives-syntax.for (const [label, value] of [  ["ten", 10n],  ["two64", 2n ** 64n],  ["negative", -5n],  ["zero", 0n],]) {  process.stdout.write("@@" + label + "\n");  %DebugPrint(value);}

Lines 3 to 6 list four BigInts: 10n, 2n ** 64n, -5n, and 0n. Line 8 prints a label before each one, so the output is easy to split. Line 9 prints how the BigInt is stored. The lesson test checks only the parts that stay the same from run to run:

  • [BigInt] and BIG_INT_BASE_TYPE: the kind of object.
  • instance size: variable: the size depends on how many digits there are.
  • The length, the sign, and the digit lines.

It does not check memory addresses or exact sizes, because those change. Chrome runs a newer V8 that also squeezes addresses into 32 bits (this is called pointer compression), so the addresses there look different.

Playground: split a BigInt into 64-bit digits
Find the 64-bit digits with BigInt mathPop out in the code editor (opens in a new tab)JavaScript
const value = 2n ** 64n;const mask = (1n << 64n) - 1n;const sign = value < 0n ? "negative" : "positive";const lowDigit = value & mask;const nextDigit = (value >> 64n) & mask; console.log(sign);console.log("0x" + lowDigit.toString(16));console.log("0x" + nextDigit.toString(16));
How it is stored18446744073709551616n
sign0
length2
bytes (estimate)40 bytes

64-bit digits, low digit first

  1. digit 00x0
  2. digit 10x1
Try it yourself

18446744073709551616n has sign 0 (not negative), length 2, and the digits 0x0, 0x1, low digit first. The byte count is only a teaching estimate, not a real measurement.

The digits are found with real BigInt & and >>. The lesson test uses %DebugPrint to check that V8 12.4 in Node stores the same digits, low digit first.

The playground finds the same digits with ordinary BigInt math:

  • value & 0xffffffffffffffffn keeps only the lowest 64 bits. That is the low digit.
  • value >> 64n drops those 64 bits, so the next digit moves down to the bottom.
  • It repeats until nothing is left. Zero has nothing to store, so it has length 0 and no digits.

The byte count it shows is a simple teaching estimate: 24 bytes plus 8 bytes per digit. It is not a real measurement of memory.

Two equal BigInts are the same keyPop out in the code editor (opens in a new tab)JavaScript
const cache = new Map([[1n, "a"]]);const set = new Set([1n, BigInt("1")]); console.log(1n === 1n);console.log(cache.get(1n));console.log(set.size);

Each BigInt lives in its own heap object, but it still acts like a plain value, the way a number does. In plain words, line by line:

  1. Line 1 makes a Map with one key, 1n, that holds "a".
  2. Line 2 makes a Set from 1n and BigInt("1"). BigInt("1") turns the text "1" into a BigInt.
  3. Line 4 prints true. === compares the two numbers, not where they are stored.
  4. Line 5 prints a. A Map finds a BigInt key by its value.
  5. Line 6 prints 1. The Set keeps one copy, because both items are the same value.

Adding goes one digit at a time

V8 adds two BigInts the way you add long numbers on paper, column by column. It adds the two low digits first. If the sum is too big for one digit, it keeps the low part and carries 1 into the next digit. Subtraction works the same way, but it borrows instead of carrying.

So the work grows with the number of digits. Adding two one-digit BigInts is quick. Adding two 1,000-digit BigInts takes about 1,000 steps.

A BigInt can never be changed after it is made. So every sum is a new BigInt, and V8 must find new memory for it. In some narrow cases, V8's optimizing compiler can skip that step.

The step-through below runs a small copy of this method, written in JavaScript. Each item in the left and right lists is one 64-bit digit, low digit first.

Step through adding one 64-bit digit at a time
Step 0 of 14Ready
Your turn: follow the blue line

Step through adding two BigInts one 64-bit digit at a time: add, keep the low 64 bits, carry the rest, then check against +.

Running in
  1. script
Next: line 25
const BIGINT_BASE = 1n << 64n;const DIGIT_MASK = BIGINT_BASE - 1n; function fromDigits(digits) {  let value = 0n;  for (let index = digits.length - 1; index >= 0; index -= 1) {    value = value * BIGINT_BASE + digits[index];  }  return value;} function addDigits(left, right) {  const out = [];  let carry = 0n;  const length = Math.max(left.length, right.length);  for (let index = 0; index < length; index += 1) {    const sum = (left[index] ?? 0n) + (right[index] ?? 0n) + carry;    out[index] = sum & DIGIT_MASK;    carry = sum >> 64n;  }  if (carry) out.push(carry);  return out;} const right = [0x2n];const digits = addDigits(left, right);console.log(digits.map((digit) => "0x" + digit.toString(16)).join(", "));console.log(fromDigits(digits) === fromDigits(left) + fromDigits(right));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Adding left and right one digit at a time. Left has digit 1 0x1 and digit 0 0xffffffffffffffff. Right has only digit 0, 0x2, so its digit 1 counts as 0. The result has digit 0 0x1, and an arrow carries 1 into digit 1, which becomes 0x2.digit 1 (high)digit 0 (low)left0x10xffffffffffffffffrightnone, so 00x2carry 1result0x20x1
Digit 0 is too big for 64 bits, so it keeps the low part, 0x1, and carries 1 to the left. Digit 1 then adds 0x1 and the carry to get 0x2.

The main work happens on three lines:

  1. Line 17 adds one digit from left, one digit from right, and the carry. A missing digit counts as 0n.
  2. Line 18 keeps only the low 64 bits of that sum, using DIGIT_MASK. That is the new digit.
  3. Line 19 shifts the sum right by 64 bits. What is left is the carry for the next digit.

Line 28 prints the new digits, 0x1, 0x2. Line 29 turns the digit lists back into BigInts with fromDigits and compares the answer with JavaScript's own +. It prints true.

Multiplying picks a method by size

Multiplying is more work than adding. The school method multiplies every digit of one number by every digit of the other. Two numbers with 100 digits each need 100 × 100 = 10,000 small multiplications. For long numbers, V8 switches to smarter methods that need fewer of them. Most are named after the people who found them:

  • Schoolbook: the method you learned at school.
  • Karatsuba: splits each number into two halves and saves one multiplication at each split.
  • Toom-Cook: splits each number into three parts.
  • FFT (fast Fourier transform): turns the multiplication into a different math problem that is quicker for huge numbers. V8's file says this method comes from Schönhage and Strassen.

V8 decides by counting the digits of the shorter number. The size where a new method starts is called a threshold. The thresholds are set in bigint-internal.h, and bigint-internal.cc uses them to choose. The 2018 V8 BigInt article describes the first version, which used only the school method.

Each line below is a constant copied from V8's source: a name and a digit count.

Thresholds copied from V8 12.4.254.21Text
V8 12.4.254.21 src/bigint/bigint-internal.h:kKaratsubaThreshold = 34 digitskToomThreshold      = 193 digitskFftThreshold       = 1500 digitskFftInnerThreshold  = 200 digitskBurnikelThreshold  = 57 digitskBarrettThreshold   = 13310 digitskToStringFastThreshold = 43 digitskFromStringLargeThreshold = 300 digits

Lines 2 to 4 are the multiplication thresholds. Line 5 is used inside the FFT method. Lines 6 and 7 are for division, and lines 8 and 9 are for strings. Both come later in this lesson.

Four method cards from left to right: Schoolbook for 2 to 33 digits, Karatsuba for 34 to 192 digits, Toom-Cook for 193 to 1499 digits, and FFT for 1500 digits and up. An arrow underneath points right: more digits.digits in the shorter numberSchoolbookKaratsubaToom-CookFFT2 to 3334 to 192193 to 14991500 and upmore digits
V8 counts the digits of the shorter number and picks a method. Each method to the right does more setup work but needs fewer small multiplications when the numbers are very long.

The table turns the thresholds into ranges. Read each row as: this method runs when the shorter number has this many digits. In the third column, n is the number of digits.

Which multiplication method V8 uses
MethodDigits in the shorter numberHow the work growsWhere it is in V8
Single digitExactly 1 digit.About n steps.MultiplySingle in src/bigint/bigint-internal.cc.
Schoolbook2 to 33 digits.About n × n steps.kKaratsubaThreshold = 34; mul-schoolbook.cc.
Karatsuba34 to 192 digits.About n^1.585 steps, fewer than n × n.kToomThreshold = 193; mul-karatsuba.cc.
Toom-Cook193 to 1499 digits.About n^1.465 steps.kFftThreshold = 1500; mul-toom.cc.
FFT1500 digits and up.Grows only a little faster than n.mul-fft.cc. On by default, except on Android.

A build setting, v8_advanced_bigint_algorithms, turns on Toom-Cook and FFT. It is on by default, except on Android. When it is off, V8 uses Karatsuba for all large products.

The step-through below does one Karatsuba split on 123456n * 789012n.

Step through one Karatsuba split
Step 0 of 11Ready
Your turn: follow the blue line

Step through one Karatsuba split: split each number in half, make z0, z2, and the middle part z1, then put them together and check.

Running in
  1. script
Next: line 14
const SPLIT_BASE = 1000n; function karatsubaOneLevel(x, y) {  const xLow = x % SPLIT_BASE;  const xHigh = x / SPLIT_BASE;  const yLow = y % SPLIT_BASE;  const yHigh = y / SPLIT_BASE;  const z0 = xLow * yLow;  const z2 = xHigh * yHigh;  const z1 = (xLow + xHigh) * (yLow + yHigh) - z2 - z0;  return z2 * SPLIT_BASE * SPLIT_BASE + z1 * SPLIT_BASE + z0;} console.log(product === 123456n * 789012n);console.log(product.toString());
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Multiplying two split numbers the school way takes four multiplications of halves. Karatsuba needs only three:

  1. Line 8 multiplies the two low halves. That is z0.
  2. Line 9 multiplies the two high halves. That is z2.
  3. Line 10 adds each number's two halves, multiplies the two sums once, then subtracts z2 and z0. That leaves the middle part, z1.
  4. Line 11 puts the three parts back together into the full product.

The example splits at 1000n, so you can read the halves in decimal. V8 splits along its 64-bit digits instead.

Division also has several methods. Read each row as: the method, when V8 uses it, and what to remember.

How V8 divides BigInts
MethodWhen V8 uses itWhat to remember
Single digitThe number you divide by has 1 digit.V8 divides by one 64-bit digit.
Schoolbook (Knuth)The number you divide by has fewer than 57 digits.kBurnikelThreshold = 57. V8's source says this method is loosely based on Go's and on Knuth's.
Burnikel-ZieglerThe number you divide by is longer, and Barrett is not picked.It splits the problem into smaller divisions. It is in div-burnikel.cc.
BarrettThe extra methods are on, the number you divide by has at least 13310 digits, and the number being divided is longer still.kBarrettThreshold = 13310. It is in div-barrett.cc.
Which multiplication method starts?
  • 10 digits
  • 33 digits
  • 34 digits
  • 192 digits
  • 193 digits
  • 1499 digits
  • 1500 digits
Try it yourself
0 of 7 correct

Each card gives the digit count of the shorter number. Sort it under the method V8 12.4 uses.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Turning BigInts into text and back

A BigInt stores its value as 64-bit binary digits, not as decimal text. So toString() must work out the decimal characters, and BigInt(text) must work out the binary digits. V8 does this in two files:

  • tostring.cc turns a BigInt into text. For a base that is a power of two, like 16, it just regroups the bits. For other bases, like 10, it splits a large number by dividing it again and again.
  • fromstring.cc reads text into a BigInt. For long text, it joins neighboring chunks in pairs, so the fast multiplication methods can help.

Read each row as: the kind of conversion, what V8 does, and why it matters.

How V8 turns BigInts into text and back
CaseWhat V8 doesWhy it matters
A base that is a power of twoRegroup the bits for bases such as 2, 8, 16, and 32.No division is needed, so it is quick.
A short number in another baseMultiply or divide one chunk at a time.This simple way is fine for short numbers.
toString on a long numberFrom kToStringFastThreshold = 43 digits, V8 switches methods.It divides by big powers of the base again and again, using Barrett division.
Reading long textFrom kFromStringLargeThreshold = 300 result digits, V8 switches methods.It joins neighboring chunks, so the fast multiplication methods can help.
Decimal text to hexadecimal and backPop out in the code editor (opens in a new tab)JavaScript
const decimal = "900719925474099312345";const id = BigInt(decimal);const hex = id.toString(16); console.log(hex);console.log(BigInt("0x" + hex) === id);

In plain words, line by line:

  1. Line 1 holds a long number as decimal text.
  2. Line 2 reads the text into a BigInt.
  3. Line 3 writes the BigInt as base 16 text.
  4. Line 5 prints 30d40000000001b6d9.
  5. Line 6 reads the base 16 text back. The "0x" in front tells BigInt() it is base 16. It prints true, so no part of the value was lost.

Printing a huge BigInt in decimal is slow because each base-10 character has to be worked out from the binary digits. No decimal text is stored ready to print.

Fixed 64-bit values and BigInt64Array

A normal BigInt grows as big as it needs. Some data has a fixed size instead: exactly 64 bits, like a number field in a network message. JavaScript has tools for that:

  • BigInt.asIntN(64, value) keeps only the low 64 bits and reads them as a signed number, one that can be negative.
  • BigInt.asUintN(64, value) keeps only the low 64 bits and reads them as an unsigned number, one that is never negative.
  • BigInt64Array and BigUint64Array are typed arrays: rows of fixed-size slots inside an ArrayBuffer, which is a block of raw bytes. Each slot is 64 bits. Reading a slot gives you a BigInt.
Look at the raw bytes behind a BigInt64ArrayPop out in the code editor (opens in a new tab)JavaScript
const buffer = new ArrayBuffer(16);const signed = new BigInt64Array(buffer);const bytes = new Uint8Array(buffer); signed[0] = -1n;signed[1] = (1n << 63n) - 1n; console.log(signed[0]);console.log(bytes.slice(0, 8).every((byte) => byte === 255));console.log(BigInt.asUintN(64, -1n));

In plain words, line by line:

  1. Line 1 makes a buffer of 16 bytes. That is room for two 64-bit slots.
  2. Line 2 views the buffer as signed 64-bit slots that hold BigInts.
  3. Line 3 views the same bytes one at a time, as numbers from 0 to 255.
  4. Line 5 writes -1n into slot 0.
  5. Line 6 writes the biggest signed 64-bit value, 263 − 1, into slot 1.
  6. Line 8 prints -1n. Reading a slot gives a BigInt, not a Number.
  7. Line 9 prints true. The first 8 bytes are all 255, so every bit is 1.
  8. Line 10 prints 18446744073709551615n. Those same 64 bits, read as unsigned, are 264 − 1.
signed[0] = -1n. A -1n card with an arrow into eight byte cards, each holding 255. Together they are one 64-bit slot. Reading signed[0] gives -1n again.signed[0] = -1n-1na BigInt255255255255255255255255one 64-bit slot: 8 bytes, each 255reading signed[0] gives -1n again
The buffer does not hold a BigInt. It holds eight plain bytes, all 255. Reading signed[0] turns those bytes back into the BigInt -1n.
What V8 speeds up

V8 keeps notes on what kind of values each operation sees. Its file type-hints.h has two such notes for BigInts: kBigInt and kBigInt64. When the values always fit in 64 bits, compiler files such as simplified-lowering.cc can use plain 64-bit machine math instead of heap BigInts, in the right cases. This is a fact about how V8 can store the values. It does not promise that every BigInt expression gets faster.

The biggest BigInt

A BigInt can hold very large numbers, but not endless ones. V8 sets BigInt::kMaxLengthBits to 1 << 30, which is 230 bits, about one billion bits. The limit is in src/objects/bigint.h. A note there says V8 picks the same limit on every platform, a little below what it could handle.

Ask for a BigInt that is too bigJavaScript
// Run in Node. V8 checks the size first, so it throws before it uses the memory.try {  2n ** (2n ** 30n);} catch (error) {  console.log(error.name + ": " + error.message);}

Line 3 asks for 2 to the power of 230. The answer would need more than 230 bits, so V8 throws an error. Line 4 catches it, and line 5 prints its name and message.

The error, checked

The lesson test runs this code in a separate Node process and checks that it prints RangeError: Maximum BigInt size exceeded. V8 checks the size before it builds the number, so the error comes at once. The test never tries to build a billion-bit number.

How other engines store BigInts

Every engine follows the same ECMAScript BigInt rules, which say how a BigInt behaves. But each engine may store BigInts in its own way. Your code can't see the difference, so don't rely on it.

Read each row as: the engine, how it stores BigInts, and what its source code shows.

How three engines store BigInts
EngineHow it stores BigIntsWhat its source shows
V8 12.4 (Chrome, Node)Every BigInt is a heap object.One field for sign and length, then 64-bit digits. No short form for small BigInts.
JavaScriptCore (Safari)A short form for small BigInts exists in the source, but it is off.USE(BIGINT32) adds a BigInt32Tag, but WebKit sets USE_BIGINT32 0.
SpiderMonkey (Firefox)A heap object that keeps a few digits inside it.JS::BigInt keeps a short digit list inside itself, and moves bigger ones to a separate list.
  • JavaScriptCore is Safari's engine. Its file JSCJSValue.h has a short form for small BigInts, with EncodeAsBigInt32 and a BigInt32Tag. It sits behind a switch called USE(BIGINT32). Right now WebKit turns that switch off with USE_BIGINT32 0.
  • SpiderMonkey is Firefox's engine. Its file BigIntType.h defines InlineDigitsLength. A small BigInt keeps its digits inside the BigInt itself. Only a bigger one gets a separate list of digits.

Practical use

Use a Number when:

  • the values stay below Number.MAX_SAFE_INTEGER, about 9 quadrillion,
  • you need decimals, like 2.5,
  • or you need Math functions, like Math.round().

Use a BigInt for exact large whole numbers, such as:

  • database IDs,
  • math for cryptography (the codes that keep data secret),
  • bit masks, which are numbers used as rows of on/off switches,
  • counters that go past 253,
  • and 64-bit fields in network messages or file formats.
Keep a 64-bit field inside 64 bitsPop out in the code editor (opens in a new tab)JavaScript
const fromProtocol = BigInt.asUintN(64, 18446744073709551616n + 5n);const smallCount = 42n; console.log(fromProtocol);console.log(Number(smallCount));

Line 1 wraps 264 + 5 into 64 bits. Only 5n is left, because the 264 part does not fit. Line 4 prints 5n. Line 5 turns the small BigInt 42n into a Number on purpose and prints 42.

  • Keep Number math and BigInt math apart. Convert on purpose, where data comes in or goes out.
  • When a value has a fixed 64-bit size, use BigInt.asUintN(64, value) or BigInt.asIntN(64, value).
  • Read a large decimal ID with BigInt(text). Don't turn it into a Number first, because that can round it.
  • Measure slow code before you change it. The thresholds and 64-bit shortcuts are facts about V8, not promises about speed.

Common misconceptions

  • “Small BigInts are stored without a heap object.” Not in V8 12.4. The tests show that 10n is a [BigInt] heap object.
  • “BigInt has no maximum.” V8 has a largest number of bits. Past it, V8 throws a RangeError.
  • “BigInt64Array keeps BigInt objects in its buffer.” The buffer holds raw 64-bit slots. Reading a slot makes a BigInt.
  • “Printing a BigInt in decimal is free.” Turning binary digits into decimal characters is real work.

Each row shows an idea, what is true, and the mistake people often make.

BigInt ideas that are easy to mix up
IdeaWhat is trueThe common mistake
Comparing BigInts1n === 1n compares the two numbers. Map and Set also compare BigInt keys by value.Thinking it checks whether both are the same stored object.
Heap objectsEvery V8 BigInt is a heap object, and math makes new ones.Thinking that this makes every BigInt operation too slow to use.
BigInt64ArrayThe array holds raw 64-bit slots in an ArrayBuffer. Reading a slot makes a BigInt.Thinking BigInt objects sit inside the buffer.
Biggest sizeV8 refuses a BigInt above 1 << 30 bits. BigInt is limited by V8 and by memory.Thinking a BigInt can grow forever.
Real-life analogyTwo forms with the same number

Two forms can both show the number 1. When you check the number, it doesn't matter which sheet of paper it is on. BigInt keys in a Map or Set work the same way.

In real life: Two forms both show 1
In JavaScript: Two expressions both give 1n
In real life: You compare the written number
In JavaScript: Map and Set compare BigInt keys by value
In real life: A changed total goes on a new form
In JavaScript: Math on a BigInt gives a new BigInt

Practice exercises

Exercise 1 · Warm-upFind the two digits of 2n ** 64n

Run the program. Type the two digits it prints, in order.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const value = 2n ** 64n;
const mask = (1n << 64n) - 1n;
console.log("0x" + (value & mask).toString(16));
console.log("0x" + ((value >> 64n) & mask).toString(16));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeLook up a BigInt key made another way

    Predict what the Map lookup prints.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const cache = new Map([[1n, "a"]]);
    console.log(cache.get(BigInt("1")));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeCarry into the next digit

      Write the low digit and the carry that the addition makes.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const mask = (1n << 64n) - 1n;
      const sum = 0xffffffffffffffffn + 2n;
      console.log("0x" + (sum & mask).toString(16));
      console.log(sum >> 64n);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeMultiply the Karatsuba example

        Run the multiplication and type the decimal product.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        console.log((123456n * 789012n).toString());

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · PracticeRead -1n as an unsigned 64-bit value

          Predict the unsigned value.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          console.log(BigInt.asUintN(64, -1n));

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeWrap a counter into 64 bits

            Use the 64-bit wrapping rule to predict what is left.

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            console.log(BigInt.asUintN(64, 18446744073709551616n + 5n));

            Answer, then press Check. Spacing and letter case don’t matter.

              Quiz

              BigInt internals quiz · 8 questionsScore: first tries count
              1. Question 1 of 8How does V8 12.4, inside Node 22, store a BigInt?

                Choose an answer to see the explanation.

              2. Question 2 of 8What length does V8 print for 0n?

                Choose an answer to see the explanation.

              3. Question 3 of 8What does this BigInt key code print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const cache = new Map([[1n, "a"]]);
                const set = new Set([1n, BigInt("1")]);
                console.log(1n === 1n);
                console.log(cache.get(1n));
                console.log(set.size);

                Choose an answer to see the explanation.

              4. Question 4 of 8In V8 12.4, how many digits must the shorter number have before V8 switches from Schoolbook to Karatsuba?

                Choose an answer to see the explanation.

              5. Question 5 of 8What does this 64-bit BigInt code print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const buffer = new ArrayBuffer(8);
                const signed = new BigInt64Array(buffer);
                signed[0] = -1n;
                console.log(signed[0]);
                console.log(BigInt.asUintN(64, signed[0]));

                Choose an answer to see the explanation.

              6. Question 6 of 8Why can big.toString(10) take a long time for a huge BigInt?

                Choose an answer to see the explanation.

              7. Question 7 of 8What error does V8 throw for 2n ** (2n ** 30n)?

                Choose an answer to see the explanation.

              8. Question 8 of 8A network message has a 64-bit number field that wraps round at the top. What should you use?

                Choose an answer to see the explanation.

              Key takeaways

              • V8 stores every BigInt as a heap object with a sign, a length, and 64-bit digits, low digit first.
              • Zero has length 0. 2n ** 64n has two digits: 0x0, then 0x1.
              • Adding takes one step per digit. Multiplying switches from Schoolbook to Karatsuba, Toom-Cook, and FFT as the numbers get longer.
              • Turning a BigInt into text, or text into a BigInt, is real work, most of all for long decimal numbers.
              • Use BigInt for exact large whole numbers. Use asUintN(64) or asIntN(64) for fixed 64-bit values.

              One-line definition: A BigInt is an exact whole number that V8 stores as a heap object with a sign, a length, and 64-bit digits, and never changes.

              Up next: Symbols inside the engine.

              CompleteFrontend Clear concepts. Working examples.