Ankit Ranjan
Back to Deep Dives

Doubles in Dart — IEEE 754, Precision, and Special Values

How a few clever switches let us store fractions, why 0.1 + 0.2 isn't 0.3, and how to work with floating-point numbers without getting bitten.

May 22, 2026 7 topics 7 quiz questions
Share:
1

Fixed point — the obvious-but-wasteful first attempt

How in the world do we represent fractions? Let's try representing them with switches.

Let's take a number — say 123.456. If we have to represent it with switches, how about this: dedicate half our switches to the integral part, and half to the fractional part, with the decimal point always sitting in the middle?

That works for 123.456. The number fits comfortably with room to spare.

But what about a really big integer, like 99 999 999? The left half overflows even though the right half is sitting empty. And the other way around: a tiny fraction like 0.00000001 overflows the right half while the left half does nothing.

Fixed point — decimal point locked in the middle8 integral switches . 8 fractional switches123.456_____123456_____✓ fits, but 10 of 16 switches sit unused99 999 999X9999999________✗ overflows the integral half, fractional half wastedFixed point wastes half our switches whenever a number isn't balanced.

This arrangement is called fixed point — the decimal point is always in the same place. And it is inefficient. We commit half our switches to each side regardless of what kind of number we are storing.

It would be great if the point could float — from the centre to the left or right, as the number demands.

2

Let the point float

Here is the idea. Instead of fixing where the decimal point lives, we let it slide.

For 123.456, the point sits between the 3 and the 4.
For 99 999 999, the point slides way to the right.
For 0.00000001, the point slides way to the left.

In each case, we use all our switches to store the actual digits — none go to waste.

To make this work, we mention all the digits together without the decimal, then set aside a few switches to store the position of the point, and one more switch for the sign.

That is the basic idea of a floating-point number:

• some bits for the digits (we call this the mantissa)
• some bits for the position of the point (the exponent)
• one bit for the sign

Most modern languages, including Dart, use a standard called IEEE 754 to define exactly how those bits are arranged. Let's look inside.

3

IEEE 754 — anatomy of a double

Dart's double type uses IEEE 754 double precision: 64 bits total, organised like this.

IEEE 754 double-precision — 64 bits, split three ways1 bit11 bits52 bitsSEXPONENTMANTISSAsign0 = positive1 = negativewhere the point sits(the scale, a power of 2)the significant binary digits(with an implicit leading 1)value = (−1)^sign × mantissa × 2^exponentRange: ±1.7 × 10³⁰⁸ · Precision: about 15–17 decimal digits

1 sign bit0 for positive, 1 for negative
11 exponent bits — how far to shift the point (as a power of 2)
52 mantissa bits — the actual significant binary digits

The number isn't stored as decimal digits, but as binary. So really we are representing a number like ± mantissa × 2^exponent.

This gives us roughly 15 to 17 significant decimal digits of precision, with a range that runs from near zero up to about 1.7 × 10³⁰⁸. For graphics, audio, scientific work, and most everyday numerical code, this is plenty.

But "about 15 to 17 digits" hides a subtle problem. Let's meet it next.

4

Why 0.1 + 0.2 isn't 0.3

Try this in DartPad:

print(0.1 + 0.2);  // 0.30000000000000004
This is not a Dart bug. It is a consequence of how binary floating-point works.

Some numbers have clean decimal representations but messy binary ones. The classic example we already know is in decimal:

Some clean fractions in one base are messy in anotherIn decimal — we already know this:1 / 3 = 0.3333333333…an infinite repeating decimal, never exactly representable with a fixed number of digitsIn binary — what doubles actually see:0.1 (decimal) = 0.000110011001100110011…an infinite repeating binary fractiona double has 52 mantissa bits, so the value gets cut off here ↓stored value ≈ 0.1000000000000000055511151231257827…

When we add 0.1 and 0.2, both values are slightly off, and the sum carries those errors forward. We get 0.30000000000000004 instead of the clean 0.3.

What this means in practice:

• Never use == to compare doubles directly. Compare with a small tolerance instead.
• For exact decimal maths (money, anything where 0.10 + 0.20 must equal exactly 0.30), use integers instead of doubles. Store cents, not dollars. Or reach for a decimal library.

// Don't:
if (price == 0.30) { ... }

// Do:
if ((price - 0.30).abs() < 1e-9) { ... }

// Even better for money:
int priceInCents = 30;

5

NaN, Infinity, and minus zero

IEEE 754 reserves some bit patterns for special values that aren't ordinary numbers. They exist so that numerical computations can keep running without crashing, even when the maths is undefined.

double inf    = 1.0 / 0.0;   // Infinity
double negInf = -1.0 / 0.0;  // -Infinity
double nan    = 0.0 / 0.0;   // NaN  (Not a Number)
double negZero = -0.0;        // -0
A few quirks worth knowing about:

NaN is not equal to anything — including itself. This trips everyone up the first time.

double x = 0.0 / 0.0;  // NaN
print(x == x);         // false!
print(x.isNaN);        // true — use this instead
So to check whether a value is NaN, always use .isNaN, never == NaN.

Positive and negative zero compare equal, but behave differently when used.

print(0.0 == -0.0);    // true
print(1 / 0.0);        // Infinity
print(1 / -0.0);       // -Infinity
Infinity is a real value in Dart. Dividing by zero does not throw — it gives us Infinity (or -Infinity), and our program keeps running. We can check with .isInfinite or .isFinite.

This is a feature, not a bug. Many numerical algorithms (especially in physics simulations and signal processing) deliberately let values flow through Infinity and NaN, checking only at the end whether the result is finite.

6

Float32List vs Float64List

Just as integers had typed list variants, dart:typed_data gives us packed, unboxed storage for floating-point numbers.

Float64List — 8 bytes per element, full IEEE 754 double precision
Float32List — 4 bytes per element, single precision (about 7 decimal digits)

Float64List matches what we get with plain double — same precision, same range, just packed contiguously in memory without boxing. This is what we want for general numeric work.

Float32List trades half the precision for half the memory. That trade is worth it in two big domains:

Graphics — vertex positions, colours, texture coordinates. GPUs work in 32-bit floats natively, so passing them Float32List avoids a conversion step.
Machine learning — neural network weights and activations, where speed and memory matter more than the last few digits of precision.

For finance, simulation, or anything where rounding errors must be kept tiny, stick with Float64List (or plain doubles).

import 'dart:typed_data';

// Vertex buffer for a 3D model
Float32List positions = Float32List(vertexCount * 3);

// Scientific computation
Float64List samples = Float64List(1000000);

7

num, conversion, parsing, and practical tips

A few last things worth knowing.

The num type. Dart has a parent type num that is the supertype of both int and double. Use it when a function should accept either:

num average(num a, num b) => (a + b) / 2;

average(3, 4.5);     // works for both
Conversion is explicit. Dart will not silently convert between int and double.

int i = 42;
double d = i;            // error
double d = i.toDouble(); // ok
int back = d.toInt();    // ok (truncates toward zero)
Parsing from strings.

double x = double.parse("3.14");
double? y = double.tryParse("hello");  // null
Comparing doubles for equality. Always use a tolerance.

bool nearlyEqual(double a, double b, [double eps = 1e-9]) =>
  (a - b).abs() < eps;
When to use what:

• Everyday numerical work, sensor readings, graphics, science → double
• Money, currency, exact decimal totals → integers (store cents) or the decimal package
• Large collections of floats → Float64List (or Float32List for graphics/ML)
• Either-int-or-double parameters → num

That is the full tour of double in Dart. We have seen how it stores fractions, why some values are inexact, and how to navigate the quirks safely. In the next episode we will get to strings — and discover that "Hello" isn't quite as simple as it looks either.

Test your understanding

7 questions

Seven questions covering floating-point representation, IEEE 754, precision pitfalls, and Dart's special double values.

Search

Loading search...