Dart (3.13) Module 2 – Values and types: variables, numbers, strings and operators
Numbers in Dart: int, double, BigInt and the web
Dart's int, double, num and BigInt: ~/ and Euclidean %, rounding, parsing, money in paise, random numbers, and where JavaScript and Wasm builds differ.
What you will learn
- Choose int, double, num or BigInt for a value, and keep money as a whole number of paise
- Predict what /, ~/, % and remainder() give for negative numbers and for zero
- Parse numbers from text with parse and tryParse, and write long literals with digit separators
- Predict which results differ between the Dart VM, a JavaScript build and a WebAssembly build, and test on all three
- Use Random with a seed for repeatable runs and Random.secure() for values nobody may guess
Before you start
What you’ll learn · Before you start
What you will learn
- Choose int, double, num or BigInt for a value, and keep money as a whole number of paise
- Predict what /, ~/, % and remainder() give for negative numbers and for zero
- Parse numbers from text with parse and tryParse, and write long literals with digit separators
- Predict which results differ between the Dart VM, a JavaScript build and a WebAssembly build, and test on all three
- Use Random with a seed for repeatable runs and Random.secure() for values nobody may guess
Before you start
On this page
In Dart, 0.1 + 0.2 prints 0.30000000000000004, and doubling 1 sixty-three times prints -9223372036854775808, a
negative number. Neither is a bug. Both follow from how Dart stores its two kinds of number, and the second answer
changes again when the same code is compiled to JavaScript.
You will pick a type for a count, a measurement and an amount of money, divide and round negative numbers without
surprises and read numbers from text safely. Then recorded runs show where the Dart VM, a JavaScript build and a
WebAssembly build part ways, and how dart test checks all three.
int counts, double measures, and num takes either
Dart stores numbers in two types: int for whole numbers and double for numbers with a fraction. The literal
decides which one you get. Digits alone make an int, in hexadecimal too, and a decimal point or an exponent makes a
double. Both are subtypes of num, the type for a value that may be either, such as a parameter that accepts 3 as
well as 2.5. Run this program and compare each value with the type it prints:
// int and double are two types of number, and num is the type above both.
void main() {
var seats = 42; // an int literal: no decimal point
var mask = 0xFF; // also an int, written in hexadecimal
var price = 1.5; // a decimal point makes a double
var big = 1.42e5; // so does an exponent
double z = 1; // an int literal where a double is expected becomes 1.0
num either = 3; // a num may hold an int ...
either += 0.5; // ... and then a double
print('$seats ${seats.runtimeType}, $mask ${mask.runtimeType}');
print('$price ${price.runtimeType}, $big ${big.runtimeType}, $z');
print('either = $either, ${either.runtimeType}');
print('7 / 2 = ${7 / 2}, 6 / 2 = ${6 / 2}'); // / always gives a double
print('1 == 1.0: ${1 == 1.0}, identical(1, 1.0): ${identical(1, 1.0)}');
print('1 is double: ${(1 as Object) is double}');
} Output
42 int, 255 int 1.5 double, 142000.0 double, 1.0 either = 3.5, double 7 / 2 = 3.5, 6 / 2 = 3.0 1 == 1.0: true, identical(1, 1.0): false 1 is double: false
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run kinds.dart
A few lines deserve a second look. double z = 1 printed 1.0, because an integer literal where a double is expected
becomes a double. 7 / 2 and 6 / 2 both printed doubles, 3.5 and 3.0: the / operator always returns a double,
even when the division comes out even. And 1 equals 1.0 with == without being identical to it, or a double at
all, since on the Dart VM: The virtual machine that runs Dart programs from source during development, compiling them just in time; dart run uses it, and it makes hot reload and debugging possible. More in “What Dart is and where it runs” one is an int object and the other a double.
Because / always gives a double, its result cannot go into an int variable:
// Half of 9 guests, as an int: the / operator does not give one.
void main() {
int perTable = 9 / 2;
print(perTable);
} Output (exit status 254)
Printed as an error (standard error)
half.dart:3:20: Error: A value of type 'double' can't be assigned to a variable of type 'int'.
int perTable = 9 / 2;
^
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run half.dart
The compiler refused with “A value of type ‘double’ can’t be assigned to a variable of type ‘int’”, and the run
ended with status 254, the status of a program that does not compile. To seat 9 guests at 2 tables, first decide
what happens to the half: 9 ~/ 2 keeps 4 and (9 / 2).round() gives 5.
Checkpoint
Check: int, double and num
An int is exact for 63 bits, then it wraps around
On the VM and in native builds an int is a 64-bit two's complement: How a native Dart int stores whole numbers in 64 bits: the top bit counts as minus 2^63, so the range runs from -2^63 to 2^63 - 1, and a result past either end wraps around to the other. integer: one
bit for the sign and 63 for the value. It holds every whole number from -2^63 to 2^63 - 1, about 9.2 × 10^18 either
way, and inside that range every result is exact. This program doubles 1 sixty-four times, prints the last four
results, and then steps past both ends of the range:
// Doubling 1 again and again: an int has 64 bits, and the top bit is the sign.
void main() {
var n = 1;
for (var step = 1; step <= 64; step++) {
n = n * 2;
if (step >= 61) print('1 doubled $step times: $n');
}
const largest = 9223372036854775807; // 2^63 - 1
const smallest = -largest - 1; // -2^63
print('largest + 1 = ${largest + 1}');
print('smallest - 1 = ${smallest - 1}');
} Output
1 doubled 61 times: 2305843009213693952 1 doubled 62 times: 4611686018427387904 1 doubled 63 times: -9223372036854775808 1 doubled 64 times: 0 largest + 1 = -9223372036854775808 smallest - 1 = 9223372036854775807
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run wrap.dart
Up to 2^62 every doubling was exact. The next one needed the top bit, which is the sign bit, so the result printed
as -9223372036854775808, the most negative int, and one more doubling left 0, because the only 1 bit had moved out
of the 64. The last two lines show the same wrap from both ends: the largest int plus 1 became the smallest, and
the smallest minus 1 became the largest, 9223372036854775807.
This is integer overflow: A whole-number result too large for its type. On native platforms a Dart int that passes 2^63 - 1 wraps around to a negative number, with no exception and no warning., and nothing reports it while the program runs: no exception and no warning, only a wrong number that looks like any other. Convert 4611686018427387904, which is 2^62, to base 2 in the number base converter and read its 64-bit signed form: a single 1, right next to the sign bit that the next doubling reaches.
Waiting for an overflow error
A product that passes 2^63 - 1 does not stop the program. It wraps, and the program carries on with the wrapped value, as the doubling did. When a whole number can grow that far, use BigInt, shown further down, or check the operands before you multiply.
Checkpoint
Check: 64-bit int overflow
Four ways to divide, and the one that never goes negative
Dart has four ways to divide whole numbers, and they agree only while both numbers are positive. This program divides 7 and -7 by 3 with each of them, counts back through a week, and divides by zero:
// Dividing 7 and -7 by 3 four ways, counting back through a week, then dividing by zero.
void main() {
for (final a in [7, -7]) {
print('$a / 3 = ${a / 3}');
print('$a ~/ 3 = ${a ~/ 3}, $a % 3 = ${a % 3}, $a.remainder(3) = ${a.remainder(3)}');
}
print('7 % -3 = ${7 % -3}, 7.remainder(-3) = ${7.remainder(-3)}');
const wednesday = 2; // days numbered 0 to 6 from Monday
print('10 days before day 2: % gives ${(wednesday - 10) % 7}, remainder gives ${(wednesday - 10).remainder(7)}');
print('5 / 0 = ${5 / 0}, -5 / 0 = ${-5 / 0}, 0 / 0 = ${0 / 0}');
try {
print(5 ~/ 0);
} on UnsupportedError catch (e) {
print('5 ~/ 0 threw $e');
}
} Output
7 / 3 = 2.3333333333333335 7 ~/ 3 = 2, 7 % 3 = 1, 7.remainder(3) = 1 -7 / 3 = -2.3333333333333335 -7 ~/ 3 = -2, -7 % 3 = 2, -7.remainder(3) = -1 7 % -3 = 1, 7.remainder(-3) = 1 10 days before day 2: % gives 6, remainder gives -1 5 / 0 = Infinity, -5 / 0 = -Infinity, 0 / 0 = NaN 5 ~/ 0 threw IntegerDivisionByZeroException
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run divide.dart
For 7 the answers match school arithmetic: 7 / 3 is 2.33…, and 7 ~/ 3 is 2 with 1 left over. For -7 they split
three ways: ~/ gave -2, remainder() gave -1 and % gave 2. Step through how each one is worked out:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The exact quotient. Ordinary division gives -2.3333333333333335, a double. Each integer operator starts from this quotient and decides what to do with its fraction.
- Rounding toward zero. The ~/ operator drops the fraction and keeps -2. It rounds toward zero, so -2.33 becomes -2 and not -3, exactly as truncate() would.
- What is left over. remainder() is what -7 still needs after three lots of -2: -7 minus -6 is -1. It always has the sign of the number being divided, here negative.
- Kept from 0 to 2. The % operator wants a result from 0 up to, but not including, 3. A negative remainder gets 3 added: -1 plus 3 is 2, because -7 equals 3 times -3, plus 2.
- Where the two differ. For 7 both give 1. They differ only when the number being divided is negative, which is when a position counted backwards needs %, as in a clock or the days of a week.
~/ rounds the quotient toward zero, and remainder() is what is left after that quotient, so it takes the sign of
the number being divided. % is the Euclidean modulo: The remainder that Dart's % operator returns: always from 0 up to, but not including, the absolute value of the divisor, so -7 % 3 is 2 while -7.remainder(3) is -1.. The API documentation
defines its result r by a == b * q + r and 0 <= r < b.abs(), and adds that “the sign of the returned value r is
always positive”, which is why even 7 % -3 gave 1.
That makes % the operator for positions that wrap around. With the days of a week numbered 0 to 6 from Monday,
10 days before Wednesday, day 2, is (2 - 10) % 7, which gave 6, a Sunday. The remainder() of the same numbers
gave -1, which is no day at all.
Division by zero splits the operators too. / follows floating-point rules: 5 / 0 is Infinity, -5 / 0 is -Infinity
and 0 / 0 is NaN, “not a number”, and the program carries on with them. 5 ~/ 0 has no int to give, so it threw
an IntegerDivisionByZeroException, which the program caught as an UnsupportedError. The changelog of Dart 2.15
deprecated that class and asks code to catch Error instead, or to check for a zero divisor before dividing.
Wrapping an index with remainder()
Stepping back from the first item of a list with (i - 1).remainder(length) gives -1, and a list has no
position -1. (i - 1) % length gives length - 1, the last position, which is what a carousel or a ring of
players needs.
Checkpoint
Check: Integer division and modulo
A double is a binary fraction, so count money in paise
A double is a 64-bit floating-point number in the IEEE 754 format, the same on every target Dart compiles to. It
holds a binary fraction, and most decimal fractions, 0.1 among them, never end when written in base 2, the way 1/3
never ends in base 10. Each is stored as the nearest double, and the small gaps show up in sums and rounding:
// A double is a binary fraction: what that does to sums, rounding and money.
void main() {
print('0.1 + 0.2 = ${0.1 + 0.2}');
print('0.1 + 0.2 == 0.3: ${0.1 + 0.2 == 0.3}');
print('close enough: ${(0.1 + 0.2 - 0.3).abs() < 1e-9}');
for (final x in [2.5, -2.5, 2.4, -2.6]) {
print('$x: round ${x.round()}, floor ${x.floor()}, ceil ${x.ceil()}, truncate ${x.truncate()}');
}
print('2.675 to 2 places: ${2.675.toStringAsFixed(2)}, 2.665: ${2.665.toStringAsFixed(2)}');
print('2.675 is stored as ${2.675.toStringAsPrecision(20)}');
// Ten items of 10 paise each, as rupees in a double and as paise in an int.
var rupees = 0.0;
var paise = 0;
for (var i = 0; i < 10; i++) {
rupees += 0.1;
paise += 10;
}
print('as double: $rupees, == 1.0: ${rupees == 1.0}');
print('as int: $paise paise, == 100: ${paise == 100}');
// 19.99 rupees to paise: toInt() cuts off the fraction, round() takes the nearest.
print('19.99 * 100 = ${19.99 * 100}');
print('toInt(): ${(19.99 * 100).toInt()}, round(): ${(19.99 * 100).round()}');
} Output
0.1 + 0.2 = 0.30000000000000004 0.1 + 0.2 == 0.3: false close enough: true 2.5: round 3, floor 2, ceil 3, truncate 2 -2.5: round -3, floor -3, ceil -2, truncate -2 2.4: round 2, floor 2, ceil 3, truncate 2 -2.6: round -3, floor -3, ceil -2, truncate -2 2.675 to 2 places: 2.67, 2.665: 2.67 2.675 is stored as 2.6749999999999998224 as double: 0.9999999999999999, == 1.0: false as int: 100 paise, == 100: true 19.99 * 100 = 1998.9999999999998 toInt(): 1998, round(): 1999
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run floats.dart
0.1 + 0.2 printed 0.30000000000000004, so it is not == to 0.3. When “close enough” is the real question, compare
the difference with a tolerance, as the third line does. Rounding has two traps of its own:
round()sends a half away from zero, so 2.5 became 3 and -2.5 became -3, whilefloor()always goes down,ceil()always goes up andtruncate()goes toward zero.toStringAsFixed(2)rounds the value that is stored, not the one you typed. 2.675 is stored as 2.6749999999999998224, so it printed 2.67, and so did 2.665.
double precision: The 64-bit IEEE 754 floating-point format of Dart's double, with 53 bits of precision; most decimal fractions, such as 0.1, are stored as the nearest binary fraction. is plenty for a length or a temperature, and the wrong tool for
money. The last lines of the program show the usual alternative: count money in its smallest unit, as an int.
Ten items of 10 paise, added as 0.1 rupees in a double, made 0.9999999999999999, which is not 1.0; added as the
int 10, they made exactly 100. Convert a typed amount once, with round(). 19.99 × 100 is
1998.9999999999998 as a double, so toInt(), which cuts the fraction off, gave 1998, one paisa short, while
round() gave 1999.
Type 0.1 or 2.675 into the IEEE 754 converter to see the exact decimal value of the double that is stored for each.
Key idea
Keep an amount of money as an int count of its smallest unit, add and compare those ints, and put the decimal point back only when you display the amount.
Checkpoint
Check: Doubles, rounding and money
Text becomes a number through parse or tryParse
Numbers from a file, a form or the command line arrive as text. int.parse and double.parse turn text into a
number and throw a FormatException when they cannot; int.tryParse and double.tryParse return null instead, which
suits input that a person typed. This program tries both on good and bad text, and then turns numbers back into text:
// Turning text into numbers, and numbers back into text.
void main() {
print(int.parse('42') + 1);
print(int.parse('0x1F')); // a 0x prefix reads hexadecimal
print(int.parse('-0x1F')); // after an optional sign
print(int.parse('ff', radix: 16)); // or name the base
print(int.parse('1010', radix: 2));
print(int.tryParse('12a')); // null instead of an exception
print(int.tryParse(' 7 ')); // spaces around the digits are allowed
print(int.tryParse('1_000')); // separators work in source code, not in text
print(double.parse('1e3'));
print(double.tryParse('3,5')); // a comma is not a decimal point
print(num.parse('8').runtimeType); // num.parse picks int or double
try {
int.parse('4.0');
} on FormatException catch (e) {
print('FormatException: ${e.message} "${e.source}"');
}
const population = 1_000_000; // digit separators in a literal
const mask = 0xFF_FF;
print('$population $mask');
print(255.toRadixString(2)); // back to text in base 2
print(7.toString().padLeft(3, '0'));
} Output
43 31 -31 255 10 null 7 null 1000.0 null int FormatException: Invalid radix-10 number "4.0" 1000000 65535 11111111 007
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run parse.dart
int.parse read a 0x prefix as hexadecimal, so ‘0x1F’ gave 31, after an optional sign too, and digits of any
base from 2 to 36 with radix. Spaces around the digits were accepted, but a letter, a comma or an underscore was
not: ‘12a’, ‘3,5’ and ‘1_000’ each gave null, and ‘4.0’ threw “Invalid radix-10 number”, since an int has no
decimal point.
num.parse('8') returned an int. Going the other way, toRadixString(2) wrote 255 in base 2 and padLeft(3, '0')
padded 7 to 007.
The underscores that int.tryParse refused are welcome in source code. Since language 3.6 a number literal may hold
digit separator: An underscore between two digits of a number literal, such as 1_000_000, allowed from language version 3.6. It makes a long literal easier to read and does not change its value., one or more underscores between two digits, and they change
nothing about the value: 1_000_000 is one million and 0xFF_FF is 65535. A separator anywhere else is an error:
// A separator must sit between two digits.
void main() {
const fee = 1_500_;
print(fee);
} Output (exit status 254)
Printed as an error (standard error)
bad_separator.dart:3:15: Error: Digit separators ('_') in a number literal can only be placed between two digits.
Try removing the '_'.
const fee = 1_500_;
^
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run bad_separator.dart
The compiler refused 1_500_ because the last underscore has no digit after it. Separators need language version 3.6,
so a package whose SDK constraint: The environment sdk entry of pubspec.yaml, such as ^3.13.0: the Dart SDK versions a package works with. Its lower bound also sets the package's language version. More in “What Dart is and where it runs” starts below 3.6 cannot use them.
Lakh and crore grouping in a literal
Separators may follow Indian grouping too: 1_00_000 is one lakh and 1_00_00_000 is one crore, which reads at a
glance in code about rupee amounts. The next program prints both and checks that a crore is ten million.
// Digit separators group a literal however its readers group it (language 3.6 or later).
void main() {
const lakh = 1_00_000; // Indian grouping
const crore = 1_00_00_000;
const million = 1_000_000; // international grouping
print('$lakh $crore $million');
print(crore == 10 * million); // the underscores change nothing
} Output
100000 10000000 1000000 true
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run separators.dart
The run printed plain digits and true: once the program runs, the underscores have left no trace.
Checkpoint
Check: Parsing and digit separators
BigInt is exact at any size, and costs more
When a whole number can pass 2^63 - 1, an int wraps, as the doubling did. BigInt: Dart's integer type of any size, limited only by memory and exact on every target, JavaScript included; it is bigger and slower than int, and its arithmetic takes only other BigInts. is the
arbitrary-precision integer of dart:core: its documentation says a big integer can have as many digits as memory
allows. This program multiplies 1 × 2 × … × 25 with both:
// 25! with int and with BigInt.
void main() {
var asInt = 1;
var asBig = BigInt.one;
for (var n = 1; n <= 25; n++) {
asInt *= n;
asBig *= BigInt.from(n);
if (n == 20 || n == 21 || n == 25) print('$n! int: $asInt BigInt: $asBig');
}
print('fits in an int: ${asBig.isValidInt}');
final parsed = BigInt.parse('15511210043330985984000000');
print('same as parsed: ${parsed == asBig}');
print('2^100 = ${BigInt.two.pow(100)}');
print('back to int: ${BigInt.from(1 << 40).toInt()}, as double: ${asBig.toDouble()}');
} Output
20! int: 2432902008176640000 BigInt: 2432902008176640000 21! int: -4249290049419214848 BigInt: 51090942171709440000 25! int: 7034535277573963776 BigInt: 15511210043330985984000000 fits in an int: false same as parsed: true 2^100 = 1267650600228229401496703205376 back to int: 1099511627776, as double: 1.5511210043330986e+25
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run factorial.dart
The two agreed up to 20!. At 21! the int had already wrapped to a negative number, and 25! as an int printed
7034535277573963776, a wrong number of plausible size, while the BigInt printed all 26 digits of
15511210043330985984000000. A BigInt’s operators take another BigInt, so an int joins in through BigInt.from(n);
BigInt.parse reads text, and toInt() and toDouble() convert back. Check isValidInt before toInt(): a value
too big for an int is clamped to the largest one, 9223372036854775807, without an error. The price is speed and
size: the Numbers in Dart page warns that BigInt and the fixnum package “often result in significantly bigger and
slower code”, so keep BigInt for values that really grow that far.
Every number type is now on the page. Answer the four questions for a value from your own code, such as a count of page views, a temperature or the price of an order, and see which types the table rules out:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Type | Exact whole numbers | Fractions | Past 2^53 in JavaScript | Sums of money | Size and speed |
|---|---|---|---|---|---|
| int Holds: whole numbers | Good exact up to 2^63 - 1 on the VM, then it wraps: 25! became 7034535277573963776 | Poor none: int perTable = 9 / 2 does not compile | Poor a JavaScript build keeps 53 bits: 2^53 + 3 printed as ...992 | Mixed exact for whole rupees, but most prices have paise | Good a signed 64-bit integer on native targets |
| int of paise Holds: money, in its smallest unit | Good every amount is a whole number of paise | Mixed two decimal places by design: 19.99 is stored as 1999 | Mixed exact up to 2^53 paise in JavaScript, far above everyday sums | Good sums stay exact: ten items of 10 paise made exactly 100 | Good plain int arithmetic |
| double Holds: binary fractions, 64 bits | Mixed exact only up to 2^53; 25! came out as 1.5511210043330986e+25 | Good measurements and ratios: 0.25, 1.42e5, sqrt(2) | Poor rounds past 2^53 on every target, not only in JavaScript | Poor ten 0.1s made 0.9999999999999999, and 0.1 + 0.2 is not 0.3 | Good the same 64-bit floating point on every target |
| num Holds: an int or a double | Mixed exact only while it holds an int | Good can hold a double | Poor it holds an int or a double, so no help past 2^53 | Poor as int or double, it brings the problems of whichever it holds | Good an int or a double underneath |
| BigInt Holds: whole numbers of any size | Good exact at any size: 25! is 15511210043330985984000000 | Poor whole numbers only | Good exact on every target, JavaScript included | Mixed exact, but heavier than an int of paise for everyday sums | Poor Dart warns of bigger and slower code; it cannot mix with int |
How the answers decide
- “Is it an amount of money?” Yes: rules out double and num. Binary fractions cannot hold most amounts with paise exactly, so their sums drift; ten 0.1s made 0.9999999999999999.
- “Is it an amount of money?” Yes and “Is the value always a whole number?” No, like a length or a ratio: rules out int and adds a caveat to int of paise. Store the amount as a whole number of paise: 19.99 becomes 1999, and only display code puts the decimal point back.
- “Is the value always a whole number?” No, like a length or a ratio and “Is it an amount of money?” No: rules out int, int of paise and BigInt. A length, a ratio or a measurement needs a fraction, which only double, or num holding one, can carry.
- “Is the value always a whole number?” Yes, like a count and “Can it grow past 2^53, about 9 followed by 15 zeros?” Yes, it can and “Will the same code run in a build compiled to JavaScript?” Yes, on the web too: rules out int, int of paise, double and num. A JavaScript build keeps 53 bits for every int, so only BigInt stays exact past 2^53 there.
- “Is the value always a whole number?” Yes, like a count and “Can it grow past 2^53, about 9 followed by 15 zeros?” Yes, it can and “Will the same code run in a build compiled to JavaScript?” No, native or WebAssembly only: rules out double and num and adds a caveat to int. A native or WebAssembly int is exact up to 2^63 - 1 and then wraps without an error; past that, use BigInt.
- “Is the value always a whole number?” Yes, like a count and “Can it grow past 2^53, about 9 followed by 15 zeros?” No, it stays far below: adds a caveat to BigInt and double. An int is exact and fast for this value; BigInt would cost size and speed for nothing, and a double hides that the value is whole.
For most values the answer is short: an int for a count, a double for a measurement, an int of paise for money,
and BigInt only past 2^63 - 1, or past 2^53 when the code also runs as JavaScript, which the next section shows.
Checkpoint
Check: BigInt and choosing a type
A JavaScript build keeps 53 bits, a WebAssembly build keeps 64
The first lesson of this track compiled one program three ways and saw 2^53 + 1 print
as 2^53 in JavaScript. The reason is what each compiler turns an int into. JavaScript has a single kind of number,
a 64-bit double, and dart2js: The Dart compiler behind dart compile js, which turns a program into JavaScript. There int and double are both JavaScript numbers, so integers are exact only up to 2^53., the compiler behind dart compile js, maps both int and
double to it, which leaves 53 bits of integer precision. dart2wasm: The Dart compiler behind dart compile wasm, which turns a program into a WebAssembly module that uses WasmGC, plus a small JavaScript file that loads it., behind dart compile wasm,
printed the VM’s 64-bit results in every run of this lesson. Step through a counter that starts at 2^53 on each
target:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The starting value. count starts at 9007199254740992, which is 2 to the power 53. Every target stores this number exactly, because it is a power of two.
- Adding 1 on the VM. On the Dart VM an int is a 64-bit two's-complement integer, 63 bits for the value and one for the sign, so 2^53 + 1 is just another exact value.
- Adding 1 in JavaScript. A JavaScript build stores every int as a 64-bit double, which has 53 bits of precision. 2^53 + 1 would need one bit more, so the sum is rounded to a double it can hold, which is 2^53 again.
- Stuck without an error. The second and third + 1 round back the same way. After three additions the VM holds 9007199254740995 and the JavaScript build still prints 9007199254740992, and neither run reported a problem.
- The WebAssembly build. dart2wasm keeps an int as a 64-bit integer, as the VM does, so the WebAssembly build printed 9007199254740995 too.
- Bits in JavaScript. Bit operations differ too. A JavaScript build works on the low 32 bits only, so 1 << 32 gave 0 and (-1).oneBitCount gave 32, where the VM and the WebAssembly build gave 4294967296 and 64.
The program behind the figure prints the counter and nine more lines of results:
import 'dart:math';
// The same expressions on the Dart VM, as JavaScript and as WebAssembly.
void main() {
var count = 9007199254740992; // 2^53
for (var i = 0; i < 3; i++) {
count += 1;
}
print('2^53 plus 1, three times: $count');
var doubled = 1;
var factorial = 1;
for (var i = 1; i <= 63; i++) {
doubled *= 2;
if (i <= 25) factorial *= i;
}
print('1 doubled 63 times: $doubled');
print('25! as an int: $factorial');
var shift = 32;
print('1 << 32: ${1 << shift}, 1 << 40: ${1 << 40}');
print('(-1).oneBitCount: ${(-1).oneBitCount}');
print('(1 << 40).trailingZeroBitCount: ${(1 << 40).trailingZeroBitCount}');
Object one = 1;
print('1 is double: ${one is double}, identical(1, 1.0): ${identical(one, 1.0)}');
print('10.0 * 2 prints as ${10.0 * 2}');
print('-7 % 3 = ${-7 % 3}, 0.1 + 0.2 = ${0.1 + 0.2}');
final r = Random(42);
print('Random(42): ${[for (var i = 0; i < 5; i++) r.nextInt(100)]}');
} The session runs it on the VM, compiles it to JavaScript and to WebAssembly, and runs both builds with Node.js 24.21.0, with a small loader for the WebAssembly module like the first lesson’s:
from session import files, run
# web/numbers.dart on the Dart VM, compiled to JavaScript and compiled to WebAssembly (both run by Node.js).
files("web/numbers.dart=numbers.dart", "web/run_wasm.mjs=run_wasm.mjs")
run(
"""
dart run numbers.dart
dart compile js -O2 -o out/numbers.js numbers.dart > /dev/null
node out/numbers.js
dart compile wasm -O2 -o out/numbers.wasm numbers.dart > /dev/null
node run_wasm.mjs
"""
) Output
$ dart run numbers.dart 2^53 plus 1, three times: 9007199254740995 1 doubled 63 times: -9223372036854775808 25! as an int: 7034535277573963776 1 << 32: 4294967296, 1 << 40: 1099511627776 (-1).oneBitCount: 64 (1 << 40).trailingZeroBitCount: 40 1 is double: false, identical(1, 1.0): false 10.0 * 2 prints as 20.0 -7 % 3 = 2, 0.1 + 0.2 = 0.30000000000000004 Random(42): [87, 58, 4, 85, 77] $ dart compile js -O2 -o out/numbers.js numbers.dart > /dev/null $ node out/numbers.js 2^53 plus 1, three times: 9007199254740992 1 doubled 63 times: 9223372036854776000 25! as an int: 1.5511210043330986e+25 1 << 32: 0, 1 << 40: 0 (-1).oneBitCount: 32 (1 << 40).trailingZeroBitCount: 32 1 is double: true, identical(1, 1.0): true 10.0 * 2 prints as 20 -7 % 3 = 2, 0.1 + 0.2 = 0.30000000000000004 Random(42): [87, 58, 4, 85, 77] $ dart compile wasm -O2 -o out/numbers.wasm numbers.dart > /dev/null $ node run_wasm.mjs 2^53 plus 1, three times: 9007199254740995 1 doubled 63 times: -9223372036854775808 25! as an int: 7034535277573963776 1 << 32: 4294967296, 1 << 40: 1099511627776 (-1).oneBitCount: 64 (1 << 40).trailingZeroBitCount: 40 1 is double: false, identical(1, 1.0): false 10.0 * 2 prints as 20.0 -7 % 3 = 2, 0.1 + 0.2 = 0.30000000000000004 Random(42): [87, 58, 4, 85, 77]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 web_session.py
The VM and the WebAssembly build printed the same ten lines. The JavaScript build differed in eight of them:
- The counter stayed at 9007199254740992, and 1 doubled 63 times printed 9223372036854776000, a rounded positive number where the other two printed the wrapped -9223372036854775808.
- 25! as an
intprinted 1.5511210043330986e+25: no wrap, but a double with 17 significant digits in place of the 26 exact ones. - Bit operations used the low 32 bits only:
1 << 32and1 << 40gave 0,(-1).oneBitCountgave 32 and(1 << 40).trailingZeroBitCountgave 32, where the others gave 4294967296, 1099511627776, 64 and 40. - 1 counted as a
double,identical(1, 1.0)was true, and 10.0 × 2 printed as 20 instead of 20.0.
A literal that JavaScript cannot hold does not even compile. odd.dart stores the odd number 9007199254740993 as a constant:
// An odd number just above 2^53, written as a literal.
void main() {
const odd = 9007199254740993;
print('$odd is odd: ${odd.isOdd}');
} from session import files, run
# A literal above 2^53: the VM runs it, dart2js refuses to compile it, dart2wasm compiles it.
files("literal/odd.dart=odd.dart", "literal/run_wasm.mjs=run_wasm.mjs")
run(
"""
dart run odd.dart
!dart compile js -o out/odd.js odd.dart
dart compile wasm -o out/odd.wasm odd.dart > /dev/null
node run_wasm.mjs
"""
) Output
$ dart run odd.dart
9007199254740993 is odd: true
$ dart compile js -o out/odd.js odd.dart
odd.dart:3:15:
Error: The integer literal 9007199254740993 can't be represented exactly in JavaScript.
const odd = 9007199254740993;
^^^^^^^^^^^^^^^^
Error: Compilation failed.
[exit status 1]
$ dart compile wasm -o out/odd.wasm odd.dart > /dev/null
$ node run_wasm.mjs
9007199254740993 is odd: true
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 literal_session.py
The VM and the WebAssembly build printed it, while dart2js stopped with “The integer literal 9007199254740993 can’t be represented exactly in JavaScript.” That error is a help, because a value computed while the program runs, like the counter, gets no warning at all.
New in Dart 3.13: oneBitCount and trailingZeroBitCount
Both getters arrived in Dart 3.13. The SDK changelog says they work on the full 64-bit two’s-complement value on native platforms and on the least significant 32 bits on the web, which is what the session printed.
Checkpoint
Check: Numbers in JavaScript and Wasm builds
dart test can run the same tests on three targets
A difference that shows only in a JavaScript build is easy to miss, because dart test runs tests on the Dart VM
unless you say otherwise. The package counting has two small functions and three tests of them:
/// Adds 1 to [start], [times] times, as a counter does.
int countUp(int start, int times) {
var n = start;
for (var i = 0; i < times; i++) {
n += 1;
}
return n;
}
/// How many flags are set in [mask], one flag per bit.
int flagsSet(int mask) => mask.oneBitCount; import 'package:counting/counting.dart';
import 'package:test/test.dart';
void main() {
test('counts small numbers', () {
expect(countUp(1, 2), 3);
});
test('counts past 2^53', () {
// Compared as text: a JavaScript build cannot even compile the literal 9007199254740995.
expect(countUp(9007199254740992, 3).toString(), '9007199254740995');
});
test('sees flags 0 and 40', () {
expect(flagsSet(1 << 40 | 1), 2);
});
} The test past 2^53 compares text, because the expected value written as an int, 9007199254740995, would stop
dart2js from compiling the test file, and none of its tests would run. The first session runs the tests on the VM,
then with -p node, which compiles them with dart2js and runs them in Node.js:
from session import files, run
# The package's tests on the Dart VM, then compiled to JavaScript and run by Node.js.
files("matrix=counting")
run(
"""
cd counting
dart pub get --offline > /dev/null
dart test
!dart test -p node
""",
pub=True,
) Output
$ cd counting
$ dart pub get --offline > /dev/null
$ dart test
--:-- +0: loading test/counting_test.dart
--:-- +0: test/counting_test.dart: counts small numbers
--:-- +1: test/counting_test.dart: counts past 2^53
--:-- +2: test/counting_test.dart: sees flags 0 and 40
--:-- +3: All tests passed!
$ dart test -p node
--:-- +0: loading test/counting_test.dart
--:-- +0: test/counting_test.dart: counts small numbers
--:-- +1: test/counting_test.dart: counts past 2^53
--:-- +1 -1: test/counting_test.dart: counts past 2^53 [E]
Expected: '9007199254740995'
Actual: '9007199254740992'
Which: is different.
Expected: ... 99254740995
Actual: ... 99254740992
^
Differ at offset 15
org-dartlang-sdk:///lib/_internal/js_runtime/lib/js_helper.dart 1424:15 Object.throwExpression
package:matcher Object.expect
test/counting_test.dart 11:5 main.<fn>
package:test_api <fn>
org-dartlang-sdk:///lib/_internal/js_runtime/lib/async_patch.dart 339:19 _wrapJsFunctionForAsync.closure.$protected
org-dartlang-sdk:///lib/_internal/js_runtime/lib/async_patch.dart 365:23 _wrapJsFunctionForAsync.<fn>
org-dartlang-sdk:///lib/_internal/js_runtime/lib/async_patch.dart 313:7 _awaitOnObject.<fn>
org-dartlang-sdk:///lib/async/zone_root.dart 48:39 StaticClosure._rootRunUnary
--:-- +1 -1: test/counting_test.dart: sees flags 0 and 40
--:-- +1 -2: test/counting_test.dart: sees flags 0 and 40 [E]
Expected: <2>
Actual: <1>
org-dartlang-sdk:///lib/_internal/js_runtime/lib/js_helper.dart 1424:15 Object.throwExpression
package:matcher Object.expect
test/counting_test.dart 15:5 main.<fn>
package:test_api <fn>
org-dartlang-sdk:///lib/_internal/js_runtime/lib/async_patch.dart 339:19 _wrapJsFunctionForAsync.closure.$protected
org-dartlang-sdk:///lib/_internal/js_runtime/lib/async_patch.dart 365:23 _wrapJsFunctionForAsync.<fn>
org-dartlang-sdk:///lib/_internal/js_runtime/lib/async_patch.dart 313:7 _awaitOnObject.<fn>
org-dartlang-sdk:///lib/async/zone_root.dart 48:39 StaticClosure._rootRunUnary
--:-- +1 -2: Some tests failed.
Failing tests:
test/counting_test.dart: counts past 2^53
test/counting_test.dart: sees flags 0 and 40
Consider enabling the flag chain-stack-traces to receive more detailed exceptions.
For example, 'dart test --chain-stack-traces'.
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 test_session.py
All three passed on the VM. Under Node.js the counter test failed with Expected ‘9007199254740995’ and Actual
‘9007199254740992’, and the flag test found 1 flag where it expected 2, because 1 << 40 is 0 in a JavaScript build.
The second session adds -c dart2wasm, which compiles the same tests to WebAssembly instead:
from session import files, run
# The same tests compiled to WebAssembly and run by Node.js.
files("matrix=counting")
run(
"""
cd counting
dart pub get --offline > /dev/null
dart test -p node -c dart2wasm
""",
pub=True,
) Output
$ cd counting $ dart pub get --offline > /dev/null $ dart test -p node -c dart2wasm --:-- +0: [Node.js, Dart2Wasm] loading test/counting_test.dart --:-- +0: [Node.js, Dart2Wasm] test/counting_test.dart: counts small numbers --:-- +1: [Node.js, Dart2Wasm] test/counting_test.dart: counts past 2^53 --:-- +2: [Node.js, Dart2Wasm] test/counting_test.dart: sees flags 0 and 40 --:-- +3: All tests passed!
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 test_wasm_session.py
All three passed again, each test name now starting with [Node.js, Dart2Wasm]. The package:test documentation adds
that one command can list several platforms, as in -p "chrome,vm", and that @TestOn limits a test file to some
platforms.
Key idea
Integer arithmetic past 2^53 and bit operations on large values can pass every test on the VM and fail in a
JavaScript build, so run such tests with -p node too, and with -c dart2wasm when you ship WebAssembly.
Checkpoint
Check: Tests on three targets
A seeded Random repeats itself, and Random.secure() keeps secrets
The dart:math library holds the rest of the everyday number tools: min and max, sqrt, pow, constants such as
pi and e, and Random, which draws random ints, doubles and bools. This program uses a few of them:
import 'dart:math';
// dart:math's functions, a seeded generator twice, and a secure one.
void main() {
print('max ${max(3, 8)}, min ${min(-2, 5)}, sqrt ${sqrt(2)}, pow ${pow(2, 10)}, pi $pi');
List<int> draw(Random r) => [for (var i = 0; i < 5; i++) r.nextInt(100)];
print('Random(42): ${draw(Random(42))}');
print('Random(42) again: ${draw(Random(42))}');
print('Random(7): ${draw(Random(7))}');
final secure = Random.secure();
final roll = secure.nextInt(6) + 1; // a different value on every run
print('secure roll is 1 to 6: ${roll >= 1 && roll <= 6}');
} Output
max 8, min -2, sqrt 1.4142135623730951, pow 1024, pi 3.141592653589793 Random(42): [87, 58, 4, 85, 77] Random(42) again: [87, 58, 4, 85, 77] Random(7): [20, 37, 78, 49, 6] secure roll is 1 to 6: true
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run random.dart
Random(42) gave 87, 58, 4, 85 and 77 twice, because a seed fixes the whole sequence, which makes a shuffle in a test
or a simulation repeatable, and Random(7) gave another five. The web session printed the same five numbers for
Random(42) on all three targets, but nothing promises that: the constructor’s documentation says “The
implementation of the random stream can change between releases of the library”. So a test may seed a generator and
check properties of what it draws, never the exact numbers.
The default generator is not for secrets either. Its documentation describes pseudo-random bits that are “not
suitable for cryptographic purposes”, and the seeded runs above show the risk: the same seed gives the same values,
so whoever learns or guesses a seed can produce them too.
Random.secure() draws from a cryptographically secure source instead, and throws an UnsupportedError on a platform
that cannot provide one. Its values change on every run, which is why the program prints only that its roll was from
1 to 6.
Pick the generator by what a guess would cost
A game, a shuffle or a test may use Random() or Random(seed). A session token, a password-reset link or anything
else an attacker gains by guessing needs Random.secure().
Checkpoint
Check: Seeded and secure random numbers
Practise: format and parse amounts in paise
Money code is where this lesson’s rules meet, so build the two functions a checkout screen needs, with no double
anywhere:
- Write
String formatPaise(int paise)with~/,%andpadLeft: the whole rupees with Indian digit grouping, a decimal point and exactly two digits of paise. - Write
int parseRupees(String s)that accepts the grouping commas, an optional rupee sign and one or two decimals, returns paise, and throws a FormatException for text such as ‘1.234’ or ‘12a’. - Put both in
lib/of a package, write five tests, and rundart test,dart test -p nodeanddart test -p node -c dart2wasm. - Add a test that formats 9007199254740993 paise, an amount above 2^53, and note which of the three runs fails, and why.
The amounts to test with
formatPaise(12345678) returns ‘₹1,23,456.78’ and formatPaise(5) returns ‘₹0.05’. parseRupees('1,23,456.78')
returns 12345678, parseRupees('₹99') returns 9900 and parseRupees('0.5') returns 50. Indian grouping puts a comma
after the last three digits of the rupees, then after every two: lakh and crore.
Then answer the self-check. It asks about the idea you know least first, and explains every answer at once:
Practice that adapts to you
Practise this lesson
20 questions about 9 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): How do % and remainder() treat a negative number in Dart?
They agree for positive numbers and differ when the number being divided is negative. remainder() belongs to truncating division: a ~/ b rounds toward zero, and a.remainder(b) is what is left, so the result has the sign of a: -7.remainder(3) is -1, because -7 ~/ 3 is -2. The % operator is the Euclidean modulo: its result is always from 0 up to, but not including, the absolute value of b, so -7 % 3 is 2, because -7 equals 3 times -3 plus 2, and 7 % -3 is 1. Use % for anything that wraps around, such as a weekday counted backwards or an index in a ring buffer, and remainder() when the result must keep the sign of the original number.
Two longer questions in the module’s bank build on the money and randomness parts of this lesson:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A bill-splitting screen divides 1,000 rupees among 3 friends. It computes 1000 / 3, shows 333.33 for each person, and the three shares add up to 999.99, one paisa short of the bill. A 5 % service charge is coming next. How do you compute every share so the shares always add up to the bill?
-
Fresher to mid
A sign-in screen texts a 6-digit code. The code is made with Random(DateTime.now().millisecondsSinceEpoch).nextInt(999999), stored as an int, and compared with int.parse of what the person types. Name every problem you see, and say what you would ship instead.
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What do int, double and num hold, and what does / return?
int holds whole numbers, double binary fractions, and num is the type above both. / always returns a double, so a whole-number result comes from ~/ or from rounding.
-
What happens when an int on the Dart VM passes 2^63 - 1?
It wraps around without an error: on the VM and in native builds an int is a 64-bit two's-complement integer, so doubling 1 sixty-three times printed -9223372036854775808.
-
What do -7 % 3 and (-7).remainder(3) return?
2 and -1. % is the Euclidean modulo, never below 0, while remainder() keeps the sign of the number divided, because -7 ~/ 3 is -2 (~/ rounds toward zero).
-
Stepping back from the first item of a list, why write (i - 1) % length?
(i - 1).remainder(length) gives -1, and a list has no position -1. (i - 1) % length gives length - 1, the last position, which a carousel or a ring of players needs.
-
How should a Dart program keep an amount of money?
As an int count of the smallest unit, such as paise, because a double cannot hold most decimal fractions exactly. Add and compare the ints, convert typed text once with round(), and put the decimal point back only for display.
-
What does int.tryParse return for text that is not a number?
null. Digit separators such as 1_000 work only in literals in the code, not in the text you parse.
-
When is BigInt worth its cost?
When a whole number can grow past the 64 bits of an int: BigInt stays exact at any size, at a cost in speed and code size.
-
How do integers differ in a JavaScript build and a WebAssembly build?
A JavaScript build keeps 53 bits of integer precision and does bit operations on 32 bits; a WebAssembly build keeps 64 bits. dart test -p node, with and without -c dart2wasm, shows the difference before users do.
Key takeaways
- An
intholds whole numbers and adoublebinary fractions,numis the type above both, and/always returns a double, so integer results come from~/or from rounding. - On the VM and in native builds an
intis a 64-bit two’s-complement integer that wraps past 2^63 - 1 without an error, which is why doubling 1 sixty-three times printed -9223372036854775808. ~/rounds toward zero andremainder()keeps the sign of the number divided, while%never goes below 0, which made(2 - 10) % 7the Sunday thatremainder()could not give.- A double cannot hold most decimal fractions exactly, so amounts of money are counted as an
intof paise and converted from typed text once, withround(). int.tryParsereturns null for text that is not a number, digit separators work only in literals, and BigInt stays exact at any size at a cost in speed and code size.- A JavaScript build keeps 53 bits of integer precision and 32-bit bit operations, a WebAssembly build keeps 64 bits,
and
dart test -p nodewith and without-c dart2wasmshows the difference before users do.
Terms in this lesson
- BigInt
- Dart's integer type of any size, limited only by memory and exact on every target, JavaScript included; it is bigger and slower than int, and its arithmetic takes only other BigInts.
- dart2js
- The Dart compiler behind dart compile js, which turns a program into JavaScript. There int and double are both JavaScript numbers, so integers are exact only up to 2^53.
- dart2wasm
- The Dart compiler behind dart compile wasm, which turns a program into a WebAssembly module that uses WasmGC, plus a small JavaScript file that loads it.
- digit separator
- An underscore between two digits of a number literal, such as 1_000_000, allowed from language version 3.6. It makes a long literal easier to read and does not change its value.
- double precision
- The 64-bit IEEE 754 floating-point format of Dart's double, with 53 bits of precision; most decimal fractions, such as 0.1, are stored as the nearest binary fraction.
- Euclidean modulo
- The remainder that Dart's % operator returns: always from 0 up to, but not including, the absolute value of the divisor, so -7 % 3 is 2 while -7.remainder(3) is -1.
- integer overflow
- A whole-number result too large for its type. On native platforms a Dart int that passes 2^63 - 1 wraps around to a negative number, with no exception and no warning.
- two's complement
- How a native Dart int stores whole numbers in 64 bits: the top bit counts as minus 2^63, so the range runs from -2^63 to 2^63 - 1, and a result past either end wraps around to the other.
References
- Built-in types (numbers) (The Dart project (Google))
- Numbers in Dart (The Dart project (Google))
- Operators (The Dart project (Google))
- num class (dart:core) (The Dart project (Google))
- num operator % (Euclidean modulo) (The Dart project (Google))
- num.remainder (The Dart project (Google))
- num operator ~/ (truncating division) (The Dart project (Google))
- BigInt.toInt (The Dart project (Google))
- num.round (The Dart project (Google))
- num.toStringAsFixed (The Dart project (Google))
- int.parse (The Dart project (Google))
- int.tryParse (The Dart project (Google))
- identical function (dart:core) (The Dart project (Google))
- BigInt class (dart:core) (The Dart project (Google))
- int.oneBitCount (The Dart project (Google))
- int.trailingZeroBitCount (The Dart project (Google))
- dart:math (The Dart project (Google))
- Random class (dart:math) (The Dart project (Google))
- Random constructor (dart:math) (The Dart project (Google))
- Random.secure constructor (dart:math) (The Dart project (Google))
- dart compile (The Dart project (Google))
- dart test (The Dart project (Google))
- package:test (running tests on Node.js) (The Dart project (Google))
- Dart SDK changelog (The Dart project (Google))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress