Lesson 07 / 21
BigInt
Cases where the safe integer range is crossed, the arbitrary-precision integer type, operator behavior, the ban on mixing types, and the serialization limit.
Contents
The previous lesson showed that the whole of the number type is the IEEE 754 double-precision representation, and that this representation carries a boundary at for integers. Above the boundary, two different integers round to the same bit pattern and come out equal.
This lesson takes up the cases where that boundary is actually crossed and the language’s answer to it: a separate primitive type carrying an integer of arbitrary size. The type itself matters, and so does the strict boundary placed between it and the number type.
Where the Boundary Is Crossed
The safe range is about nine quadrillion. This value, which looks distant in everyday calculations, is crossed with a single reading in certain kinds of data.
- Nanosecond-resolution timestamps. The number of nanoseconds elapsed since a start point is a million times larger than the same value in milliseconds, and it exits the range.
- 64-bit integer identifiers coming from other systems. Even if no arithmetic is done on the identifier, it has to be read back and written out without losing value.
- Counters accumulating over a long period. In a high-sample-rate measurement network, the total sample count can reach this range.
What all three share is that precision has to be preserved down to the last digit. When working with the number type, the loss is silent; no error is thrown, only the wrong value is carried.
A Separate Primitive Type
BigInt is the primitive type that carries an integer of arbitrary digit count. It is
written in two forms: by appending n to the end of a number literal, or through a
conversion call.
const a = 9007199254740993n; const b = BigInt("9007199254740993"); console.log(a === b); console.log(typeof a); console.log(a + 1n);
true bigint 9007199254740994n
In the previous lesson, this value could not be represented with the number type; here
it is preserved exactly. The n suffix in the output is not part of the value, it is the
runtime’s inspection format. The suffix disappears when the value is converted to text:
console.log(String(10n)); console.log((10n).toString()); console.log(10n);
10 10 10n
The first two lines are the result of the language’s text-conversion rule, and are the same in every environment. The display on the third line is the environment’s inspection format. The distinction established in the Ways to Run Code lesson shows up again here.
Operator Behavior
Arithmetic operators work with integer semantics. The most noticeable difference is in division:
console.log(7n / 2n); console.log(7n % 2n); console.log(2n ** 64n);
3n 1n 18446744073709551616n
Division produces no fraction; the result is truncated toward zero. The distinction between the two division operations established in the Basic Data Types lesson of the Programming Fundamentals course applies here — with the BigInt type there is only integer division. The truncation direction is toward zero, not downward; with negative operands, this distinction changes the result.
The exponentiation operator gives exactly a magnitude the number type could not represent. has twenty digits and is correct down to the last digit.
Types Cannot Be Mixed
The two number types cannot come together in arithmetic operators:
try { console.log(1n + 1); } catch (error) { console.log(error.name + ": " + error.message); }
TypeError: Cannot mix BigInt and other types, use explicit conversions
This ban is not a shortcoming, it is a deliberate design decision. The two types’ semantics do not agree: division produces a fraction in the number type, it does not in BigInt; large values round in the number type, they do not in BigInt. Which rule a mixed expression should be evaluated under cannot be chosen in a single consistent way. The language leaves the choice to the program’s author and demands an explicit conversion.
Comparison operators behave differently. Operators that compare value can take the two types together; the operator that also compares type keeps them apart:
console.log(1n == 1); console.log(1n === 1); console.log(2n > 1);
true false true
The first and third lines compare the mathematical value. The second line gives false because it also compares the types. These three operators’ rules will be defined step by step in the Equality Comparisons lesson.
The Cost of Conversion
Conversion between the two types can be written explicitly, but not every direction is safe.
Converting BigInt to number loses precision for values outside the range:
const timeNs = 1700000000123456789n; console.log(Number(timeNs)); console.log(BigInt(Number(timeNs)));
1700000000123456800 1700000000123456768n
The two lines have to be read together. The first line is the text form of the number
that comes out of the conversion; text conversion chooses the shortest decimal
representation that rounds back to the same bit pattern, which is why it looks round. The
second line writes the same value with a lossless type and shows the actual value:
...768. So the original value has been lost, and the amount of the loss is also
different from what it looks like at first glance.
Converting number to BigInt rejects fractional values:
try { console.log(BigInt(1.5)); } catch (error) { console.log(error.name + ": " + error.message); }
RangeError: The number 1.5 cannot be converted to a BigInt because it is not an integer
Built-in methods that work with the number type also do not accept BigInt:
try { console.log(Math.max(1n, 2n)); } catch (error) { console.log(error.name + ": " + error.message); }
TypeError: Cannot convert a BigInt value to a number
The entire Math object is defined for floating-point numbers. If a comparison with
BigInt values is needed, comparison operators are used.
Application to the Measurement Script
When a nanosecond-resolution timestamp is added to measurement records, the two types stand side by side in the same record:
const readings = [ { station: "A1", timeNs: 1700000000123456789n, temperature: 21.5 }, { station: "A1", timeNs: 1700000000923456789n, temperature: 21.7 }, ]; const diff = readings[1].timeNs - readings[0].timeNs; console.log("diff (ns):", diff.toString()); console.log("diff (ms):", (diff / 1000000n).toString());
diff (ns): 800000000 diff (ms): 800
The time difference was computed at nanosecond precision, then converted to milliseconds. The fraction was dropped in the conversion — integer division truncates. If a fractional result is needed, the division is done after switching to the number type, and at that point the loss of precision is knowingly accepted.
The temperature field stayed in the number type; that is the right choice for a floating-point magnitude. The two types being present in the same record is not a problem — the problem is mixing the two in the same expression.
The Serialization Limit
BigInt values prevent data from being written in JSON format:
try { console.log(JSON.stringify({ counter: 10n })); } catch (error) { console.log(error.name + ": " + error.message); } console.log(JSON.stringify({ counter: String(10n) }));
TypeError: Do not know how to serialize a BigInt
{"counter":"10"}
The reason is not a shortcoming, it is the format’s definition: JSON’s number representation carries no precision guarantee, and the reading side may convert the value to the number type. Writing it as text to carry the value losslessly is the solution that accepts the format’s boundary. JSON’s boundaries are the subject of a separate lesson.
Summary
- The safe integer range is crossed in nanosecond timestamps, 64-bit external identifiers, and counters accumulating over a long period; the loss is silent.
- BigInt is a separate primitive type; it is written with the
nsuffix or through explicit conversion, and it carries an arbitrary number of digits. - Division produces no fraction, it truncates toward zero;
Mathmethods do not work with this type. - The two number types cannot be mixed in arithmetic operators; comparison operators work together for value comparison, the operator that also compares type keeps them apart.
- Converting BigInt to number loses precision outside the range; it cannot be written directly to JSON, it is carried as text.
Next Step
This lesson showed the one case that requires an explicit conversion between the two
types to be written. In the rest of the language, the situation is the reverse: types are
converted on their own in most operators, and the result depends on the order in which
the conversion is done. The next lesson will define the steps of this implicit coercion;
the result of expressions like [] + {} will be read not as something to memorize, but
as the output of a traceable procedure.
To keep your progress and take notes, Log in
My notes
Log in to take notes.