Skip to content
Software Atlas

Numbers in Computers

How computers store whole numbers and fractions in a fixed number of bits: binary and hex, integer overflow, floating point, why 0.1 + 0.2 isn't 0.3, and how to handle money, big IDs and timestamps without losing a cent.

⏱ ~40 min read13 sections6 interactive widgetsGo · JavaScript
FoundationsPractitionerStaff-level
Foundations§01

The 30-second version

In one sentenceA computer stores every number in a fixed-size box of bits. Whole numbers (integers) are stored exactly, but the box can run out of room. Fractions are stored as floating-point numbers, which cover a huge range but are usually slightly off.

A few lines show both problems. JS has no 8-bit integer type, so its first line shows a cousin of the wrap-around: a +1 that vanishes. Run them and you get answers that look like bugs:

surprise.go
var level int8 = 127 // int8: a whole number stored in 8 bits
level++
fmt.Println(level) // -128   ← it ran out of room and wrapped around

a, b := 0.1, 0.2
fmt.Println(a + b) // 0.30000000000000004   ← not 0.3

None of these are bugs in Go or JavaScript. They follow directly from how numbers are stored, and by the end of this page you'll be able to predict every one of them.

1

Integers wrap

A fixed number of bits gives a fixed range. Go past the top and you land at the bottom, silently (§03–§04).
2

Floats round

0.1 has no exact binary form, just as 1/3 has no exact decimal form. Small errors appear and add up (§05–§08).
3

Money is integers

Count cents as whole numbers, or use a decimal type. Never store money in a float (§09).

Why you should care

These aren't trivia. They cause real bugs: a shopping cart total that's off by a cent, a 64-bit database ID that turns into a different ID once it reaches the browser, a counter that suddenly goes negative, a test that fails because 0.1 + 0.2 !== 0.3. Each section below ends with the practical rule you need.

Foundations§02

Bits, binary and hex in five minutes

In one sentenceComputers count in base 2 instead of base 10. It's the same idea as the decimal digits you already know, just with two digits (0 and 1) instead of ten.

Place values, the way you already know them

In decimal, each position is worth ten times the one to its right. The number 305 means 3 hundreds, 0 tens and 5 ones: 3×100 + 0×10 + 5×1.

Binary works the same way, but each position is worth twice the one to its right: 1, 2, 4, 8, 16, 32, 64, 128… Each binary digit is called a Bit. So 1101 in binary means 1×8 + 1×4 + 0×2 + 1×1 = 13.

Position value1286432168421Total
13000011018 + 4 + 1 = 13
20011001000128 + 64 + 8 = 200
25511111111255 (all on)

Eight bits make a Byte. With all eight switched on you get 128 + 64 + … + 1 = 255, so one byte holds 256 different values: 0 through 255. In general, n bits hold 2ⁿ values. Every extra bit doubles the range.

Try it. Click the bits to build 200, then 255:

One byte, eight switches
Click a bit to flip it. Each bit is worth twice the one to its right.
128
64
32
16
8
4
2
1
hex digit 4
hex digit 9
Binary
0b01001001
Hex
0x49
Decimal (uint8)
73
64 + 8 + 1 = 73

Hex: binary, but shorter to write

Long rows of 0s and 1s are hard to read. Hexadecimal (base 16) fixes that. It uses the digits 0–9 and then a–f for 10–15, and one hex digit is exactly 4 bits. So a byte is always exactly two hex digits: 1100 1000 → c8, and 1111 1111 → ff.

Binary00000001…1001101010111100110111101111
Hex01…9abcdef

That's why you see hex everywhere bits matter: colors (#ff8800 is three bytes: red ff, green 88, blue 00), memory addresses, hashes, and byte dumps.

Writing and printing them in code

bases.go
fmt.Println(0b1100_1000, 0xC8, 200) // 200 200 200  ← three ways to write one number
fmt.Println(1_000_000)               // 1000000      ← _ is only for readability

fmt.Printf("%b %x %08b\n", 200, 200, 5) // 11001000 c8 00000101
fmt.Println(strconv.FormatInt(255, 2))  // 11111111
Foundations§03

Fixed-width integers and negative numbers

In one sentenceAn integer type is a box with a fixed number of bits. More bits, bigger range. The bits for negative numbers use a clever trick called two's complement.

Go: you pick the box size

Go has an integer type for each common size. The number in the name is how many bits it uses:

Go typeBitsSmallestLargest
int88−128127
uint8 (alias byte)80255
int1616−32,76832,767
uint1616065,535
int32 (alias rune)32−2,147,483,6482,147,483,647
uint323204,294,967,295
int6464−9,223,372,036,854,775,8089,223,372,036,854,775,807
uint6464018,446,744,073,709,551,615
int, uint64 on 64-bit machines (32 on old 32-bit ones)

The u means unsigned: no negative numbers, so the same bits reach twice as high. The plain types are signed: half the values are negative. In everyday Go code, just use int (it's 64 bits on any machine you're likely to run). Reach for the sized types when the size is part of the job: a file format, a network protocol, a database column, or a huge array where memory matters.

Notice the ranges are lopsided: int8 goes down to −128 but only up to 127. The reason is how negatives are stored.

Two's complement: how negatives are stored

You might guess "use the first bit as a minus sign". Computers do something smarter. In Two's complement, the top bit simply has a negative place value. In an 8-bit signed number, the place values are:

Position value−1286432168421Total
5000001014 + 1 = 5
−511111011−128 + 64 + 32 + 16 + 8 + 2 + 1 = −5
−111111111−128 + 127 = −1
−12810000000−128
12701111111127

So any number with the top bit on is negative, and the smallest value is the top bit alone: −128. The largest is everything except the top bit: 127. There's one more negative than positive because zero takes one of the "top bit off" slots.

The same eight bits mean different things depending on the type. 11111011 is 251 as a uint8 and −5 as an int8. Switch the reading below and watch the same bits change meaning:

One byte, eight switches
Click a bit to flip it. Each bit is worth twice the one to its right.
-128
64
32
16
8
4
2
1
hex digit f
hex digit b
Binary
0b11111011
Hex
0xfb
Decimal (int8)
-5
-128 + 64 + 32 + 16 + 8 + 2 + 1 = -5
The top bit is on, so in int8 it counts as −128 instead of +128. That's two's complement: the same eight bits read as uint8 would be 251.

JavaScript: one number type for everything

JavaScript has no int8 or int64. Every ordinary JS number is a 64-bit float (the type §05 explains), even 1 or 42. It stores whole numbers exactly up to Number.MAX_SAFE_INTEGER, which is 2⁵³ − 1 = 9,007,199,254,740,991. That's about 9 million billion, plenty for counters and array indexes, but smaller than the int64 maximum.

For bigger whole numbers JS has a second type, BigInt, written with an n on the end: 9007199254740993n. It has no size limit. §04 shows when you need it.

sizes.go
fmt.Println(math.MaxInt8, math.MinInt8)  // 127 -128
fmt.Println(math.MaxInt64)               // 9223372036854775807
fmt.Println(uint64(math.MaxUint64))      // 18446744073709551615

var n int8 = -5
fmt.Printf("%08b\n", uint8(n)) // 11111011  ← the bits of -5, read as unsigned
Practitioner§04

Overflow: when the odometer rolls over

In one sentenceWhen a result doesn't fit in its box, Go keeps only the bits that fit and wraps around, with no error. JavaScript numbers don't wrap; past 2⁵³ they silently lose precision instead.

Go: wraps at runtime, refuses at compile time

Integer overflow in Go is simple and unforgiving. The CPU computes the result, keeps the lowest 8 (or 16, 32, 64) bits, and throws the rest away. For an int8, 127 + 1 gives the bits 10000000, which int8 reads as −128. Drive the odometer past the edge yourself:

The overflow odometer
A byte has 256 positions arranged in a circle. Step past the end and you come out the other side.
064-128-64127 | −128
var x int8
120
bits in memory
01111000
Press x + 1 until you pass 127. Go does exactly this at runtime: no error, no warning.

Green half: top bit 0 (zero and positive). Red half: top bit 1 (negative). 127 is 01111111; one more is 10000000, which int8 reads as −128.

wrap.go
var x int8 = 127
x++
fmt.Println(x) // -128  ← no panic, no error

var stock uint32 = 5
stock -= 10        // "should" be -5, but uint32 has no negatives
fmt.Println(stock) // 4294967291  ← wrapped to near the top

var big int64 = 3_000_000_000
fmt.Println(int32(big)) // -1294967296  ← converting to a smaller type drops the top bits

The uint32 line is the classic real-world bug: subtracting from an unsigned counter, a size or a length gives a huge positive number instead of a negative one.

There's one place Go does catch overflow: constants. A literal or constant expression is checked by the Compiler, so this doesn't even build:

const.go
var level int8 = 127 + 1
// compile error: cannot use 127 + 1 (untyped int constant 128) as int8 value
// in variable declaration (overflows)

JavaScript: no wrapping, but a precision cliff

A plain JS number is a float (§05 explains the format), so it never wraps around. Instead, past 2⁵³ − 1 it can no longer store every whole number. Numbers in that range are rounded to the nearest even one, then the nearest multiple of 4, and so on:

int64.go
var n int64 = 1 << 53 // 9007199254740992
fmt.Println(n + 1)     // 9007199254740993  ← int64 is exact all the way to 9.2 × 10^18

This is quieter than wrapping: the answer is close, so nothing looks wrong. It matters for 64-bit IDs and large counters, which §10 covers.

When you need bigger numbers

Both languages have a type with no size limit. It's slower than a fixed-size integer, so use it only when the numbers really can grow beyond 64 bits (cryptography, exact factorials, arbitrary user input):

big.go
n := new(big.Int)
n.SetString("9223372036854775807", 10) // math.MaxInt64
n.Add(n, big.NewInt(1))
fmt.Println(n) // 9223372036854775808  ← no wrap

p := new(big.Int).Exp(big.NewInt(2), big.NewInt(100), nil)
fmt.Println(p) // 1267650600228229401496703205376

Checking for overflow yourself

When a value comes from outside (a quantity in a request, a size read from a file), check before you compute:

checked.go
var ErrOverflow = errors.New("integer overflow")

// addInt64 returns a+b, or an error if the true sum doesn't fit in int64.
func addInt64(a, b int64) (int64, error) {
	if (b > 0 && a > math.MaxInt64-b) || (b < 0 && a < math.MinInt64-b) {
		return 0, ErrOverflow // ← test with subtraction, which can't overflow here
	}
	return a + b, nil
}

You can't check a + b > math.MaxInt64 in Go: if it overflowed, the sum has already wrapped to a negative number. Rearranging to a > math.MaxInt64 - b asks the same question without overflowing.

Overflow in the real world

1996

Ariane 5, flight 501

About 37 seconds after launch, the rocket's guidance software converted a 64-bit floating-point value to a 16-bit signed integer. The value was larger than 32,767. Unlike Go, the language used (Ada) did detect the overflow: it raised an error that nobody handled, so both inertial reference computers shut down, and the flight computer then steered using their diagnostic output as if it were flight data. The rocket veered off course and broke up. The code had been reused from Ariane 4, where that value never got so large. The lesson: catching overflow isn't enough on its own; you also need a plan for what happens next.

2014

YouTube and “Gangnam Style”

YouTube's view counter was a signed 32-bit integer, which tops out at 2,147,483,647. When PSY's video approached that number, YouTube announced it had switched the counter to 64 bits. It was caught in time, but it shows how "two billion will never happen" assumptions age.

2015

Boeing 787 power units

The US aviation regulator (FAA) issued Airworthiness Directive 2015-09-07: a software counter in the 787's generator control units would overflow after 248 days of continuous power, and that could cause the loss of all AC electrical power. Until a software fix was available, operators had to power the electrical system down at least every 120 days, comfortably before the counter could reach its limit. 248 days is roughly 2³¹ hundredths of a second, which suggests a signed 32-bit counter of centiseconds.

Practitioner§05

Floating point: scientific notation in binary

In one sentenceA Floating-point number number is scientific notation in binary: a sign, some significant digits, and an exponent that slides the point left or right. The "point floats", hence the name.

Start from scientific notation

You already know this format from science class. Avogadro's number is written 6.02 × 10²³ instead of 602,000,000,000,000,000,000,000. It has three parts:

  • a sign (+ or −),
  • the significant digits, 6.02,
  • an exponent, 23, that says how far to move the decimal point.

The trick is that a few digits plus an exponent can describe huge and tiny numbers alike. The cost is that you only keep a few significant digits. 602,214,076,000,000,000,000,000 and 602,000,000,000,000,000,000,001 both become 6.02 × 10²³ if you only keep three.

The same idea, in 64 bits

A Go float64 and every JS number use the layout fixed by the IEEE 754 standard. The 64 bits are split into three fields:

FieldBitsJob
sign10 = positive, 1 = negative
exponent11which power of two to multiply by (stored with 1023 added, so it's never negative)
fraction52the significant digits, in binary

The value is ± 1.fraction × 2^(exponent − 1023). There's a free bonus bit: in binary scientific notation the leading digit is always 1 (just as in decimal it's never 0), so it isn't stored at all. That gives 53 bits of precision from 52 stored bits, which is exactly where 2⁵³, JavaScript's safe-integer limit from §03, comes from.

Worked example: packing 5 by hand

That formula packs three new ideas into one line, so here it is step by step for the number 5:

StepWhat happensResult for 5
1. Write it in binary5 = 4 + 1101
2. Slide the point until one 1 is left in front of it101. → 1.01, which moved the point 2 places left, so multiply by 2² to make up for it1.01 × 2²
3. Sign bit5 is positive0
4. Exponent field: add 1023 (the bias), so the stored number is never negative2 + 1023 = 102510000000001 (11 bits)
5. Fraction field: drop the leading 1. (it's always there), keep what follows, pad to 52 bits.010100000…000 (52 bits)

To decode, run it backwards: the exponent field 1025 − 1023 = 2; put the hidden 1 back in front of the fraction to get 1.01 in binary, which is 1 + 0 × ½ + 1 × ¼ = 1.25; and 1.25 × 2² = 5. The digits after a binary point are worth ½, ¼, ⅛… the same way digits after a decimal point are worth tenths and hundredths.

Type 5 into the viewer below and check every field against the table. Then try 1, 0.1, 0.3 and the 0.1 + 0.2 button:

Inside a float64
Type a number (or pick one) and see the 64 bits a Go float64 or a JS number stores. Click any bit to flip it.
sign · 1 bit
0 → positive
exponent · 11 bits
1019 − 1023 = -4 → ×2-4
fraction · 52 bits
1.100110011001100110011001100110011001100110011001101 (binary) ≈ 1.6000000000000001
+1.6000000000000001 × 2-4 is stored exactly as:
0.1000000000000000055511151231257827021181583404541015625
That's not exactly what you typed (0.1): it can't be stored in 64 bits, so the nearest float64 is stored instead.
JavaScript prints it as 0.1 — the shortest text that maps back to these bits.
Gap to the next float up: 1.3877787807814457e-17

Things worth noticing as you click around:

  • 1 and −2.5 are stored exactly. They are made of powers of two (2.5 = 2 + 0.5). Same for any whole number up to 2⁵³.
  • 0.1 is not. In binary it's 0.000110011001100… forever. After sliding the point (1.100110011… × 2⁻⁴), the stored fraction bits read 1001 1001 1001…: the same repeating cycle, just counted from a different starting bit. It gets cut off after 52 bits, so the stored value is 0.1000000000000000055511151231257827… §06 explains why and what that does to 0.1 + 0.2.
  • 2⁵³ + 1 turns into 2⁵³. The 53 bits of precision are used up, so the last digit can't be stored.
  • The gap grows. The gap to the next float is about 1.4 × 10⁻¹⁷ near 0.1, about 2.2 × 10⁻¹⁶ near 1, and exactly 2 near 10¹⁶.

Precision vs range

That last point is the central trade-off. Every range between two powers of two (1 to 2, 2 to 4, 4 to 8…) holds the same number of floats: 2⁵² of them. Each range is twice as wide as the previous one, so the floats in it are twice as far apart:

Floats get sparser as they grow
01248gap 1/16gap 1/8gap 1/4gap 1/2Any value between two dots is rounded to the nearest dot.
A toy float with only 3 fraction bits: every range between two powers of two holds the same 8 values, but each range is twice as wide as the last, so the gaps double. float64 works the same way with 252 values per range: the gap is about 2.2 × 10−16 near 1 and exactly 2 near 1016.

The gap between neighbouring floats has a name, the ULP. Any result that falls between two floats is rounded to the nearest one, so every operation can be off by up to half a ULP. In practice:

float64 (Go float64, every JS number)float32 (Go float32, JS Float32Array)
Significant decimal digitsabout 15–17about 6–9
Largest valueabout 1.8 × 10³⁰⁸about 3.4 × 10³⁸
Smallest positive valueabout 5 × 10⁻³²⁴about 1.4 × 10⁻⁴⁵
Whole numbers exact up to2⁵³ ≈ 9 × 10¹⁵2²⁴ = 16,777,216

Why does §03 say the safe limit is 2⁵³ − 1 when this table says whole numbers are exact up to 2⁵³? Because 2⁵³ itself is stored exactly, but so is nothing between it and 2⁵³ + 2: the literal 9007199254740993 (2⁵³ + 1) is silently stored as 2⁵³. "Safe" means every whole number up to here is exact and can't be confused with a neighbour, so it stops one short, at 2⁵³ − 1.

Relative precision stays roughly the same everywhere: about 16 significant digits. Absolute precision depends on size. Measuring a distance in metres, float64 is accurate to well under a nanometre for the size of a room, but only to about a millimetre for the distance to Pluto (roughly 6 × 10¹² metres). That's usually fine for physics, graphics and statistics, and never fine for money (§09).

float32.go
var f32 float32 = 0.1
fmt.Println(f32)          // 0.1  ← printed with float32 precision
fmt.Println(float64(f32)) // 0.10000000149011612  ← the error is much bigger than float64's

fmt.Println(math.MaxFloat64)               // 1.7976931348623157e+308
fmt.Println(math.SmallestNonzeroFloat64)   // 5e-324
fmt.Println(math.Nextafter(1, 2) - 1)      // 2.220446049250313e-16  ← the gap above 1
Practitioner§06

Why 0.1 + 0.2 isn't 0.3

In one sentence0.1 can't be written exactly in binary, just as 1/3 can't be written exactly in decimal. So the computer stores the nearest float, and those tiny errors show up when you add.

The 1/3 problem, in base 2

Write 1/3 as a decimal: 0.3333… The 3s never end. With 10 digits of paper you have to stop at 0.3333333333, which is a little too small. Add three of those and you get 0.9999999999, not 1.

Decimal fractions work when the denominator divides into a power of ten: ½ = 0.5, ¼ = 0.25, ⅕ = 0.2. Binary fractions only work when the denominator is a power of two: ½, ¼, ⅛… And 0.1 is 1/10, and 10 = 2 × 5. The 5 doesn't go into any power of two, so in binary 0.1 repeats forever:

0.1 (decimal) = 0.0001100110011001100110011001100110011…  (binary: after "0.0", the block "0011" repeats)

A float64 keeps 52 fraction bits and rounds the rest. The same happens to 0.2, 0.3, 0.7, and to almost every decimal with digits after the point. Only fractions built from halves, quarters, eighths and so on (0.5, 0.25, 0.375) are exact.

Step through it

Here is what happens in each line of 0.1 + 0.2, with the exact stored values. Use Next or the arrow keys:

0.1 + 0.2, one rounding at a time
What is actually stored at each line. Bold digits are where the stored value differs from the decimal you typed.
Code
1const a = 0.1;
2const b = 0.2;
3const sum = a + b;
4console.log(sum); // 0.30000000000000004
5console.log(sum === 0.3); // false
stored inputs (exact)
a ≈ 0.1000000000000000055511151231257827…
the addition
not yet
console output
nothing yet
0.1 in binary is 0.0001100110011…: after 0.0, the block 0011 repeats forever, like 1/3 = 0.333… in decimal. A float64 keeps only 52 fraction bits, so it stores the nearest float64 instead. That value is a tiny bit too big: off by about 5.6 × 10⁻¹⁸.
1 / 6

So three things combined: 0.1 was stored a bit high, 0.2 was stored a bit high, and the sum was rounded up again. The result lands on a different float from the one 0.3 is stored as. Go and JavaScript give the identical answer because both follow IEEE 754, which fixes exactly how every addition must round.

Go's twist: constants are exact

Here's a Go surprise. The same expression gives two different answers depending on whether it uses constants or variables:

consts.go
fmt.Println(0.1 + 0.2)        // 0.3  ← constants: computed exactly, at compile time
fmt.Println(0.1+0.2 == 0.3)   // true

a, b := 0.1, 0.2              // now they're float64 variables
fmt.Println(a + b)            // 0.30000000000000004  ← real float64 addition
fmt.Println(a+b == 0.3)       // false

A literal like 0.1 in Go is an Untyped constant: a value that doesn't have a type such as float64 yet. The part that matters here is that numeric constants are exact. The Go spec requires the compiler to compute them with far more precision than float64 (at least 256 bits), and a value like 1/10 is kept exactly. The compiler does constant arithmetic exactly, and only rounds to float64 at the very end, when the value is assigned or printed. So 0.1 + 0.2 is computed as exactly 3/10 and then rounded once, to the same float64 as 0.3. As soon as a variable is involved, every step is float64 arithmetic with its own rounding.

Comparing floats: use a tolerance

Because results can be off by a tiny amount, comparing floats with == is almost always a bug. Check whether they're close enough instead:

almost.go
// almostEqual reports whether a and b differ by at most tol, relative to their size.
func almostEqual(a, b, tol float64) bool {
	return math.Abs(a-b) <= tol*math.Max(math.Abs(a), math.Abs(b))
}

x, y := 0.1, 0.2
fmt.Println(x+y == 0.3)                   // false
fmt.Println(almostEqual(x+y, 0.3, 1e-9))  // true

The tolerance is relative (multiplied by the size of the numbers) on purpose. A fixed tolerance like Math.abs(a - b) < Number.EPSILON works near 1 but is useless for large numbers: near a million, neighbouring floats are already 10⁻¹⁰ apart, far more than 2.2 × 10⁻¹⁶. Pick tol for your problem: 1e-9 is a common default.

One catch: relative tolerance breaks down when the numbers are close to zero. If you expect a result of exactly 0, compare against a small absolute value instead, like Math.abs(x) < 1e-12.

Practitioner§07

Infinity, NaN and negative zero

In one sentenceFloats have a few special values for results that aren't ordinary numbers: +Infinity, −Infinity, NaN ("not a number") and −0. They keep a calculation going instead of crashing, so you have to check for them yourself.

Where they come from

In the float64 layout from §05, an exponent with all 11 bits set to 1 is reserved for special values (click Infinity or NaN in the §05 viewer to see it). With a zero fraction it means Infinity; with any other fraction it means NaN. A float division by zero doesn't crash; it gives one of these. Integer division by zero is different, because integers have no bit pattern to represent "infinity":

special.go
zero := 0.0
fmt.Println(1/zero, -1/zero, zero/zero) // +Inf -Inf NaN  ← floats: no crash

n := 0
fmt.Println(10 / n) // panic: runtime error: integer divide by zero
// (and 10 / 0 written as constants doesn't compile at all)

In JavaScript, the most common source of NaN is not maths at all. It's Number(undefined), parseInt("") or "abc" * 2: some text or a missing field that didn't convert. Once NaN appears, every calculation it touches becomes NaN, so a single bad input can turn a whole report into NaN.

NaN is not equal to anything, not even itself

The standard defines every comparison with NaN as false (<, >, <=, >= and even NaN == NaN). The one exception is "not equal": NaN != NaN is true. So the only safe way to test for it is a dedicated function:

nan.go
x := math.NaN()
fmt.Println(x == x)        // false  ← the one value not equal to itself
fmt.Println(math.IsNaN(x)) // true

fmt.Println(math.IsInf(math.Inf(1), 0)) // true  ← 0 = either sign

Negative zero

Because floats have a sign bit, there are two zeros: +0 and −0. They compare equal, but they aren't the same bits, and dividing by them shows the difference:

negzero.go
nz := math.Copysign(0, -1)     // the way to get -0 in Go
fmt.Println(nz, nz == 0)       // -0 true
fmt.Println(1/nz)              // -Inf
fmt.Println(math.Signbit(nz))  // true

−0 rarely matters, but it does show up in output: a temperature of "−0 °C" after rounding −0.4, or a chart axis labelled "−0". Normalize with x + 0 (which turns −0 into +0) or x === 0 ? 0 : x before displaying.

JSON has no NaN or Infinity

JSON numbers can only be ordinary finite numbers. The two languages handle that differently, and the JS way silently loses data:

json.go
_, err := json.Marshal(math.NaN())
fmt.Println(err) // json: unsupported value: NaN  ← refuses, loudly
Practitioner§08

Small errors add up

In one sentenceEach float operation can be off by half a gap. One error is invisible, but a running total that adds thousands of values collects thousands of errors.

Watch a total drift

Here are two running totals of the same payments. The left one adds 0.1 to a float, the way you'd write total += price with prices in dollars. The right one adds 10 to a whole number of cents. Press + 1 payment a few times, then + 1,000:

Add it up: float vs integer cents
Two running totals of the same payments. One is a float (dollars), one is a whole number of cents.
let total = 0.0 · total += 0.1
0
nothing added yet
let cents = 0 · cents += 10
0
= $0.00, always exact (whole numbers up to 253)
payments: 0
#float totalcents totalequal?
Press “+ 1 payment” a few times. With $0.10 the float total first goes wrong at payment 3.

After 10 payments of $0.10 the float total is 0.9999999999999999, not 1. After 1,000 more it's off by about 10⁻¹². The integer total is exact every time, because whole numbers up to 2⁵³ are stored exactly and adding them never rounds. That's the core argument for counting money in cents (§09).

Big numbers swallow small ones

Remember from §05 that the gap between floats grows with their size. Near 10¹⁶ the gap is 2, so adding 1 changes nothing: the exact result falls between two floats and rounds back to where it started. This is sometimes called absorption:

absorb.go
huge := 1e16
fmt.Println(huge+1 == huge) // true  ← the +1 was rounded away

total := 1e16
for range 1000 {
	total += 1 // each +1 is lost on its own…
}
fmt.Println(total) // 1e+16  ← …so a thousand of them are lost too

Adding the thousand 1s together first (giving 1000) and then adding that to 10¹⁶ gives the right answer, 10000000000001000. Same numbers, different order, different result.

The order of additions matters

In school maths, (a + b) + c always equals a + (b + c). With floats it doesn't, because each + rounds its own result:

order.go
a, b, c := 0.1, 0.2, 0.3
fmt.Println((a+b)+c) // 0.6000000000000001
fmt.Println(a+(b+c)) // 0.6

This has a practical consequence: summing the same list in a different order can give a different total. Splitting a sum across goroutines or workers, letting a database add rows in whatever order it reads them, or sorting a list before summing can all change the last digits. If two systems must agree exactly, floats are the wrong tool.

Summing more accurately

When you do have to add many floats (statistics, scientific data), two cheap tricks help:

  • Add small numbers first. Sorting by size before adding reduces absorption.
  • Use compensated summation. Kahan summation keeps a second variable that remembers the part each addition rounded away and adds it back next time:
kahan.go
naive := 0.0
sum, comp := 0.0, 0.0 // comp remembers the lost low-order part
for range 10_000_000 {
	naive += 0.1

	y := 0.1 - comp      // correct the next value by what was lost last time
	t := sum + y         // this addition rounds…
	comp = (t - sum) - y // …and this recovers exactly what it rounded away
	sum = t
}
fmt.Println(naive) // 999999.9998389754
fmt.Println(sum)   // 1e+06

Ten million additions of 0.1: the naive total is off by about 0.00016, while the Kahan total prints as exactly one million. It costs a few extra operations per addition. Some languages go further and give a correctly rounded sum, as if every addition were done exactly and rounded once at the end: Python has math.fsum, and JavaScript added Math.sumPrecise([…]) in ES2026. As of October 2026 it's in Chrome 147+, Firefox 137+ and Safari 26.2+, but not in Node.js yet (Node 22 doesn't have it), so check typeof Math.sumPrecise before relying on it. Go has no built-in equivalent; use Kahan summation or math/big.

Practitioner§09

Money is never a float

In one sentenceStore money as a whole number of the smallest unit (cents for dollars), or in a decimal type. A float can't even store $0.10 exactly, and in money every cent has to be accounted for.

Why floats fail for money

Everything from §06 and §08 applies, but money adds a hard requirement: totals must match exactly. An invoice whose line items add up to $99.99 while the total says $100.00 isn't "close enough"; it's a bug report, a failed reconciliation, or an angry customer. And the float errors are easy to trigger:

float-money.go
price := 19.99
fmt.Println(price * 100)   // 1998.9999999999998  ← not 1999
fmt.Println(int(price * 100)) // 1998  ← int() cuts off the fraction: a cent vanished

The fix: integer minor units

Count money in Minor units: the currency's smallest unit, as an integer. $19.99 is 1999 cents. Integers add, subtract and compare exactly (up to 2⁵³ in JS, which is about 90 trillion dollars in cents; up to 9.2 × 10¹⁸ in a Go int64). Always store the currency next to the amount, because "1999" means nothing on its own:

money.go
type Money struct {
	Amount   int64  // minor units: 1999 = $19.99
	Currency string // ISO 4217 code: "USD"
}

cart := []Money{{1999, "USD"}, {500, "USD"}, {1, "USD"}}
var total int64
for _, m := range cart {
	total += m.Amount // exact, every time
}
fmt.Println(total) // 2500

Dividing by 100 for display is fine: the result is only shown, never stored or added up again.

Not every currency has cents

"Multiply by 100" is a bug waiting to happen, because the number of digits after the point depends on the currency. That number is the currency exponent, defined by the ISO 4217 standard:

CurrencyExponentMinor unit10 whole units are stored as
Japanese yen (JPY)0none, ¥1 is the smallest10
US dollar (USD), euro (EUR)2cent1000
Kuwaiti dinar (KWD), Bahraini dinar (BHD)3fils10000

Keep a table of exponents per currency (or use a library that has one) and convert with it. Payment providers that take amounts "in the smallest currency unit" mean exactly this.

Reading amounts without ever touching a float

User input and JSON often contain text like "19.99". Don't run it through parseFloat and multiply. Split the text at the point and build the integer directly:

parse.go
// ParseCents turns "19.99" into 1999 without ever using a float.
func ParseCents(s string) (int64, error) {
	whole, frac, _ := strings.Cut(s, ".") // "19.99" → "19", "99"
	if whole == "" {
		return 0, fmt.Errorf("not an amount: %q", s) // "" or ".5" must not become $0
	}
	if len(frac) > 2 {
		return 0, fmt.Errorf("too many decimals: %q", s)
	}
	frac += "00"[len(frac):] // pad: "9" → "90", "" → "00"
	return strconv.ParseInt(whole+frac, 10, 64) // "1999" → 1999
}

fmt.Println(ParseCents("19.99")) // 1999 <nil>
fmt.Println(ParseCents("5"))     // 500 <nil>
fmt.Println(ParseCents("1.999")) // 0 too many decimals: "1.999"
fmt.Println(ParseCents(""))      // 0 not an amount: ""

Rounding: pick the rule on purpose

Sometimes you must produce fractions of a cent: 8.875% sales tax on $19.99 is 177.41125 cents. You have to round, and there's more than one way to do it. That choice is called the rounding mode, and the default differs between languages:

ValueHalf up (toward +∞)Half away from zero (Go math.Round)JS Math.roundHalf to even (Go math.RoundToEven)
2.53332
3.54444
−2.5−2−3−2−2

The last column is banker's rounding: exact halves go to the nearest even number. Rounding every half up adds a tiny upward bias that grows over millions of transactions; half-to-even sends half the ties up and half down, so they cancel out. Many financial and accounting systems require it. Whatever you choose, round once, at a defined step (for example per line item, or per invoice), write that rule down, and do it in integer arithmetic:

tax.go
// mulDivHalfEven returns amount × num / den, rounded half to even. Inputs must be ≥ 0.
func mulDivHalfEven(amount, num, den int64) int64 {
	q, r := amount*num/den, amount*num%den // quotient and remainder, both exact
	if 2*r > den || (2*r == den && q%2 == 1) { // more than half, or exactly half and q is odd
		q++
	}
	return q
}

tax := mulDivHalfEven(1999, 8875, 100_000) // 8.875% = 8875 / 100000
fmt.Println(tax) // 177  (exact: 177.41125)

// The ties from the table, in cents: 2.5 and 3.5 cents rounded to whole cents
fmt.Println(mulDivHalfEven(250, 1, 100), mulDivHalfEven(350, 1, 100)) // 2 4

amount*num can overflow for very large amounts (§04): with int64 it's safe while the product stays below 9.2 × 10¹⁸. Use math/big or a decimal library beyond that.

JavaScript has no half-to-even rounding function for values, but for display Intl.NumberFormat accepts a rounding mode:

display.js
const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", roundingMode: "halfEven" });
console.log(usd.format(0.125), usd.format(0.375)); // $0.12 $0.38  ← ties go to the even cent

(0.125 and 0.375 are used because they're exact in binary, so they really are ties. Keep the stored amount in integer cents and use this only to format it.)

Splitting without losing a cent

Dividing $100.00 between 3 people gives $33.333… each. Round each share to $33.33 and the three shares add up to $99.99: a cent disappeared. The fix is to work in minor units and hand the leftover units out one at a time. Try different amounts, currencies and group sizes:

Split the bill
Divide an amount between people so the shares add up to exactly the total.
between3people
USD has 2 digits after the point, so 1 minor unit = $0.01. Total in minor units: 10,000
✕ float: (total / 3).toFixed(2)
$33.33$33.33$33.33
Sum: $99.99 — $0.01 vanished.
✓ integers: base + remainder
$33.34$33.33$33.33
10,000 ÷ 3 = 3,333 each, remainder 1 → one person pays one extra minor unit (green). Sum: $100.00 ✓
split.go
// Split divides total (in minor units) into n parts that add up exactly.
func Split(total int64, n int) []int64 {
	base := total / int64(n)  // 10000 / 3 = 3333
	extra := total % int64(n) // 10000 % 3 = 1 unit left over
	parts := make([]int64, n)
	for i := range parts {
		parts[i] = base
		if int64(i) < extra {
			parts[i]++ // hand out the leftover units, one each
		}
	}
	return parts
}

fmt.Println(Split(10000, 3)) // [3334 3333 3333]  ← adds up to 10000

The same idea handles uneven splits (60% / 40%, or by weight): compute each share rounded down, then give the leftover units to the shares with the biggest remainders. These helpers assume a positive total; refunds and other negative amounts need the sign handled separately (§11 explains why % of a negative number needs care).

Decimal types: when integers aren't enough

Integer minor units cover most systems. For interest rates, currency conversion, or prices with more decimals than the currency has (fuel at $3.459 per gallon), use a decimal type, which stores base-10 digits exactly:

  • Go: github.com/shopspring/decimal is the common choice; math/big's big.Rat stores exact fractions and is in the standard library.
  • JavaScript: no built-in decimal yet (there's a proposal, but it isn't part of the language). Libraries such as decimal.js and big.js fill the gap, and dinero.js wraps money specifically.
  • Databases: store money as BIGINT minor units or NUMERIC / DECIMAL. Never REAL, FLOAT or DOUBLE PRECISION, which are floats.
rat.go
a := big.NewRat(1, 10) // exactly 1/10
b := big.NewRat(2, 10) // exactly 2/10
sum := new(big.Rat).Add(a, b)
fmt.Println(sum, sum.FloatString(2)) // 3/10 0.30  ← exact

Pass decimal libraries strings (or integers), never the result of float arithmetic: new Decimal(19.99 * 100) faithfully stores 1998.9999999999998, because the damage was done before the library saw it. Money and the requests that move it also need protection against being applied twice when a client retries; Idempotency covers that side.

Staff-level§10

Numbers crossing boundaries: JSON, IDs and timestamps

In one sentenceInside one program, a number has one type. The moment it crosses into JSON, another language, a database or a file, it can be silently converted, and the most common casualty is a 64-bit integer arriving in JavaScript.

The 64-bit ID problem

JSON numbers are just digits in text. The JSON format sets no size limit; each parser decides what type to turn them into. JSON.parse turns every number into a float64. So a 64-bit ID from a Go or Java backend, a database BIGINT, or a Snowflake ID generator arrives in the browser slightly changed, and nothing tells you:

ids.go
var anything map[string]any
json.Unmarshal([]byte(`{"id": 9007199254740993}`), &anything)
fmt.Println(anything["id"]) // 9.007199254740992e+15  ← Go loses it too, into `any`

var typed struct {
	ID int64 `json:"id"`
}
json.Unmarshal([]byte(`{"id": 9007199254740993}`), &typed)
fmt.Println(typed.ID) // 9007199254740993  ← exact: decoded straight into int64

Notice the Go side: decoding into a typed int64 field is exact, but decoding into any (a map[string]any, for example) picks float64 for every number and has the same problem as JavaScript. This is a classic bug in Go proxies and middleware that pass JSON through without knowing its shape.

The fixes

Send large IDs as strings. An ID is a name, not a quantity: you never add two IDs together. A string is the honest type for it, and every language reads it unchanged.

string-id.go
type Order struct {
	ID    int64 `json:"id,string"` // ← ",string": write and read the number as a JSON string
	Total int64 `json:"total_cents"`
}

out, _ := json.Marshal(Order{ID: 9007199254740993, Total: 1999})
fmt.Println(string(out)) // {"id":"9007199254740993","total_cents":1999}

// Passing JSON through without a struct? Keep numbers as text:
dec := json.NewDecoder(strings.NewReader(`{"id": 9007199254740993}`))
dec.UseNumber() // numbers become json.Number (a string type), not float64
var m map[string]any
dec.Decode(&m)
fmt.Println(m["id"]) // 9007199254740993

Amounts of money in JSON follow the same logic from the other side: send integer minor units plus a currency ({"total_cents": 4850, "currency": "USD"}), as API Design recommends. The Text & Encodings topic covers JSON vs binary formats more broadly.

Seconds or milliseconds?

Unix time counts from 1970-01-01 UTC, but some systems count in seconds (Unix tools, most databases, JWT exp, Go's time.Unix) and others in milliseconds (JavaScript's Date.now(), Java, many JSON APIs). Mix them up and you get a date in 1970 or tens of thousands of years in the future:

units.go
fmt.Println(time.Unix(1700000000, 0).UTC())        // 2023-11-14 22:13:20 +0000 UTC  ← seconds
fmt.Println(time.UnixMilli(1700000000000).UTC())   // 2023-11-14 22:13:20 +0000 UTC  ← milliseconds

Put the unit in the field name (created_at_ms, expires_in_s) or, better, send RFC 3339 text like 2023-11-14T22:13:20Z, which carries no unit ambiguity at all. A quick sanity check: right now, seconds are a 10-digit number and milliseconds a 13-digit one.

The year 2038

Many older C programs, file formats and databases store Unix time as a signed 32-bit number of seconds. Its maximum, 2,147,483,647, is reached at 03:14:07 UTC on 19 January 2038. One second later it wraps, exactly like the odometer in §04, to the most negative value, which is a date in 1901:

y2038.go
var t32 int32 = math.MaxInt32
fmt.Println(time.Unix(int64(t32), 0).UTC()) // 2038-01-19 03:14:07 +0000 UTC
t32++                                        // wraps to -2147483648
fmt.Println(time.Unix(int64(t32), 0).UTC()) // 1901-12-13 20:45:52 +0000 UTC

That's the year 2038 problem. Modern 64-bit operating systems and Go (time.Time, Unix() returns int64) are fine, and JavaScript's Date counts milliseconds in a float64, and the language spec caps it at ±100 million days from 1970 (±8.64 × 10¹⁵ ms): the year 275760. The risk lives in old embedded devices, 32-bit binary file formats and database columns declared as 32-bit integers. It's not hypothetical for things that look ahead: a 20-year certificate or mortgage end date already crosses 2038.

Byte order in binary formats

A 4-byte integer has to be written as 4 bytes in some order, and computers don't agree on which. That's Endianness: big-endian writes the most significant byte first (the way we write digits), little-endian writes it last. Most CPUs today (x86, ARM in its usual mode) are little-endian; network protocols traditionally use big-endian. JSON and other text formats don't care, but the moment you write raw bytes you must pick one, and both sides must agree:

endian.go
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, 0x0A0B0C0D)
fmt.Printf("% x\n", buf) // 0a 0b 0c 0d
binary.LittleEndian.PutUint32(buf, 0x0A0B0C0D)
fmt.Printf("% x\n", buf) // 0d 0c 0b 0a  ← same number, bytes reversed
Staff-level§11

Staff-level corners

In one sentenceMost number bugs in production aren't about exotic maths. They come from conversions (float to int, signed to unsigned), assumptions (that % is never negative, that two machines get the same float), and unchecked inputs.

Division and remainders of negative numbers

Two beginner traps first. In Go, dividing two integers gives an integer and drops the fraction: 7 / 2 is 3. In JS, / always gives a float: 7 / 2 is 3.5. Port code between the two and this alone causes bugs.

For negative numbers, both languages truncate toward zero, and the remainder % takes the sign of the left side:

negdiv.go
fmt.Println(7/2, -7/2) // 3 -3   ← integer division truncates toward zero
fmt.Println(-7 % 2)    // -1     ← sign follows the left operand
fmt.Println(7 % -2)    // 1

You'll often read that Modulo (a % n) always lands between 0 and n − 1. That's only true when a is zero or positive: in Go and JS, a negative a gives a negative (or zero) remainder. It bites when you use % to wrap around, for example to step backwards through a circular list. (0 - 1) % 5 is -1, not 4, and indexing with −1 panics in Go or returns undefined in JS. The fix is the same in both languages:

wrap.go
// mod returns a value in 0…n-1 even when a is negative (n > 0).
func mod(a, n int) int {
	return (a%n + n) % n
}

fmt.Println((0-1)%5, mod(0-1, 5)) // -1 4

(Python's % already works like mod here, which is one reason code ported from Python breaks.)

Float-to-int conversions

Converting a float to an integer truncates toward zero in both languages: int(3.9) is 3 and int(-3.9) is −3. Round explicitly first (math.Round, Math.round) if that's what you mean. The dangerous cases are values that don't fit:

  • Go: for a float variable that's out of range for the target type (or NaN), the result is "implementation-dependent", in the words of the Go spec. On amd64, f := 1e20 followed by int64(f) gives −9223372036854775808; another CPU may give something else. (Writing int64(1e20) directly doesn't compile: for a constant, the compiler checks the range and refuses.) There's no panic and no error, so check the range before converting.
  • JS: x | 0 wraps modulo 2³² ((2 ** 32 + 5) | 0 is 5), and parseInt works on text, so parseInt(0.0000005) is 5: the number is first turned into the string "5e-7". Use Math.trunc(x) to truncate a number.

Performance: choose for correctness

On a modern CPU, integer and float addition and multiplication take a handful of CPU cycles either way, and division is slow for both (tens of cycles, roughly). Converting between the two also costs a little. These differences almost never matter next to memory access or I/O, so pick the type that is correct for the data, not the one you think is faster. Two exceptions worth knowing:

  • Arbitrary-size numbers are much slower. math/big and BigInt allocate memory as they grow; a loop over BigInts can be 10–100 times slower than the same loop over plain numbers (approximate; measure your case).
  • JS engines optimize small integers. V8 stores small whole numbers (roughly 31 or 32 bits, depending on the build) directly instead of as full floats. Code that keeps numbers small and whole stays on that fast path. That's an engine detail, not a reason to avoid fractions where you need them.

Same code, different answer on another machine?

The basic operations (+, −, ×, ÷, square root) are required by IEEE 754 to be correctly rounded, so the same operations on the same inputs in the same order give bit-identical results everywhere. Differences creep in from three places:

  1. Fused operations. Some CPUs have an FMA instruction that computes x*y + z with one rounding instead of two. The Go spec explicitly allows the compiler to use it, and it does on arm64 and some other platforms, so x*y + z can differ in the last bit between an amd64 laptop and an arm64 server. Writing float64(x*y) + z forces the intermediate rounding and prevents the fusion. JS engines aren't allowed to fuse.
  2. Library functions. Math.sin, Math.exp, math.Pow and friends aren't required to be correctly rounded. Different JS engines, Go versions or operating systems may differ in the last bit.
  3. Order of operations. As §08 showed, float addition depends on order. Parallel sums, database aggregates and anything that iterates in an unspecified order can change the result. A Go-specific trap: map iteration order is deliberately randomized.
maporder.go
amounts := map[string]float64{"a": 0.1, "b": 0.2, "c": 0.3, "d": 1e16, "e": -1e16, "f": 0.7}
totals := map[float64]bool{}
for range 1000 {
	sum := 0.0
	for _, x := range amounts { // Go randomizes map iteration order
		sum += x
	}
	totals[sum] = true
}
fmt.Println(totals)
// one run's output (yours may differ, because the order is random):
// map[0:true 0.7:true 0.7999999999999999:true 1:true 1.3:true 2:true]
// ← six different totals from the same six numbers (the true sum is 1.3)

When results must match across machines or runs (replays and lockstep multiplayer games, financial reconciliation, consensus between servers, golden-file tests), avoid floats in the critical path: use integers or fixed-point (an integer that counts thousandths, for example), and sort before summing.

Overflow is a security bug

Every overflow from §04 becomes a vulnerability when an attacker controls the input:

  • Negative quantities. An order for −3 items at $10 each is a $30 credit. Validate ranges, not just types.
  • Unsigned underflow. balance - amount on a uint64 with amount > balance doesn't go negative; it becomes about 1.8 × 10¹⁹, and a later >= check passes.
  • Size calculations. count * itemSize that wraps to a small number makes a program allocate a small buffer and then write a large amount of data. In C this corrupts memory; Go's bounds checks turn it into a panic instead, which is still a crash an attacker can trigger.

The fix is boring and effective: validate numbers at the edge of your system (minimum, maximum, integer or not), using the checked arithmetic from §04 wherever a product or sum of untrusted values could get large.

Practitioner§12

Check yourself

Predict the answer before you click. Each explanation points back to the section that covers it.

QUESTION 1 / 7
A byte that keeps counting
var stock uint8 = 250
stock += 10
fmt.Println(stock)
QUESTION 2 / 7
Three dimes
console.log(0.1 * 3 === 0.3);
QUESTION 3 / 7
Constants vs variables
x := 0.1
fmt.Println(0.1+0.2 == 0.3, x+0.2 == 0.3)
QUESTION 4 / 7
A big ID from an API
const order = JSON.parse('{"id": 9007199254740993}');
console.log(order.id);
QUESTION 5 / 7
Is it NaN?
const x = 0 / 0;
console.log(x === x, Number.isNaN(x));
QUESTION 6 / 7
Splitting $10.00 three ways
total := int64(1000) // cents
base, extra := total/3, total%3
fmt.Println(base, extra)
QUESTION 7 / 7
Remainders of negatives
console.log(-7 % 3);
Answered 0 / 7
Foundations§13

Cheat sheet

🔢
n bits, 2ⁿ valuesA byte (8 bits) holds 0–255, written as two hex digits 00–ff. Every extra bit doubles the range.
➖
Two's complementThe top bit counts as negative: int8 is −128…127. The same bits mean different numbers as signed and unsigned.
🔄
Go integers wrapAt runtime, silently: an int8 holding 127, plus 1, is −128; a uint holding 0, minus 1, is huge. Constant overflow is a compile error.
🧗
JS has a precision cliffNumbers are float64: whole numbers are exact up to 2⁵³ − 1. Beyond that, use BigInt. Bitwise ops are 32-bit.
🎯
Floats are approximateSign + 11-bit exponent + 52-bit fraction ≈ 16 significant digits. 0.1, 0.2, 0.3 are all stored slightly off.
⚖️
Never compare floats with ==Use a relative tolerance. In Go, constant expressions are exact; float64 variables are not.
🚫
NaN ≠ NaNTest with Number.isNaN / math.IsNaN. Float ÷ 0 gives ±Inf or NaN; integer ÷ 0 panics.
💵
Money = integersMinor units + currency code, or a decimal type. Look up the currency exponent. Round once, on purpose. Split by handing out the remainder.
I want to…GoJavaScript
Store a count or indexintnumber (exact up to 2⁵³ − 1)
Store a 64-bit IDint64string (or BigInt)
Send a big ID in JSONjson:"id,string"a string field
Decode unknown JSON without losing digitsDecoder.UseNumber()JSON.parse reviver with context.source
Numbers beyond 64 bitsmath/big (big.Int)BigInt (123n)
Store moneyint64 minor units, or shopspring/decimalinteger minor units, or decimal.js
Compare two floatsmath.Abs(a-b) <= tol*math.Max(…)Math.abs(a - b) <= tol * Math.max(…)
Test for NaNmath.IsNaN(x)Number.isNaN(x)
Round half to evenintegers: mulDivHalfEven (§09); non-money floats: math.RoundToEven(x)display: Intl.NumberFormat with roundingMode: "halfEven"; values: a decimal library
Wrap an index going backwards(a%n + n) % n((a % n) + n) % n
Check before adding untrusted intscompare with math.MaxInt64 - b firstNumber.isSafeInteger(sum)
Store a timestamptime.Time (64-bit), RFC 3339 in JSONDate (ms), RFC 3339 in JSON
Finished reading?Mark it complete to track your progress on a roadmap. Saved in this browser only.