Number Bases: Hex, Two's Complement, and Float Errors

Published 2026-10-31

Converting between bases is school-level arithmetic. What actually causes production bugs is everything downstream: how negatives are stored, why decimals are not exact, and when a large integer quietly changes value.

The mapping worth memorising

Decimal Binary Hex
0 0000 0
5 0101 5
10 1010 A
15 1111 F
255 1111 1111 FF
4096 1 0000 0000 0000 1000

Hex exists because 4 binary digits map exactly to 1 hex digit. One byte (8 bits) is exactly two hex digits — which is why #FF8800 reads as three bytes: 255, 136, 0.

Negative numbers: two's complement

There is no minus sign in memory. Negatives use two's complement: invert every bit, then add one.

 5  = 0000 0101
~5  = 1111 1010
+1  = 1111 1011   → 0xFB  (= -5 in 8-bit)

The payoff is that subtraction becomes addition: 7 + (-5) is 0000 0111 + 1111 1011 = 1 0000 0010, and the overflow bit is simply dropped.

The cost is an asymmetric range: 8 bits cover -128 … 127, not -127 … 127. -128 has no positive counterpart, which produces a genuine bug:

Math.abs(-2147483648) // still -2147483648

Why 0.1 + 0.2 !== 0.3

0.1 + 0.2 // 0.30000000000000004

This is not a JavaScript bug. Every language using IEEE 754 doubles behaves the same way.

Binary can only represent fractions whose denominator is a power of two. 0.5, 0.25, 0.125 are exact. 0.1 is 1/10, and 10 has a prime factor of 5, so in binary it is a repeating fraction:

0.1 (decimal) = 0.0001100110011001100... (binary, repeating)

Stored in 64 bits, it is truncated to an approximation slightly larger than 0.1. Add two approximations and the error becomes visible.

Scenario Fix
Money Use integer cents. Store 1234, divide by 100 only for display
Comparison Math.abs(a - b) < 1e-9 instead of ===
Display toFixed(2) / Intl.NumberFormat, applied only at the end
High precision A decimal library (decimal.js, big.js)

The dangerous case is accumulating money in floats — ten thousand additions of 0.01 drift by cents, and cents are what reconciliation disputes are made of.

Integers above 2^53 are not safe

JavaScript's Number is a double, so exact integers stop at 2^53 - 1 = 9007199254740991 (16 decimal digits).

9007199254740993                              // 9007199254740992
JSON.parse('{"id":9007199254740993}').id      // 9007199254740992

Snowflake IDs, 18-digit order numbers and 18-digit national ID numbers all sit past that boundary. This is why "the last two digits of the ID changed in the frontend".

The fix is at the API boundary: serialise large integers as strings, and never Number() them on the client. When you genuinely need arithmetic, use BigInt.

Two parseInt traps

parseInt('12px')    // 12 — stops at the first invalid character, no error
parseInt('')        // NaN
Number('')          // 0     ← different from parseInt
Number(null)        // 0
Number(undefined)   // NaN

parseInt('12px') === 12 is the dangerous one: malformed input passes validation silently. If you need strictness, validate the shape with a regex first, then use Number().

Bitwise operators truncate to 32 bits

JavaScript bitwise operators (& | ^ << >>) convert operands to 32-bit signed integers first, discarding anything above bit 31:

1 << 31        // -2147483648, not 2147483648
2 ** 32 | 0    // 0

So use them only when you know the values fit in 32 bits: permission bitmasks, colour channel packing, small hashes. Also note >> preserves the sign bit while >>> fills with zeros — using the wrong one turns "take the high byte" into a negative number.

Checking by hand

To verify a hex value, see what a byte sequence means in decimal, or convert a batch of numbers, use Base Converter — binary, octal, decimal and hex update together.

For bulk numeric cleanup (padding, truncating, thousands separators), Number Tools is faster than spreadsheet formulas.

Three rules to keep:

  1. Two hex digits = one byte; hex is just shorthand for binary.
  2. Floats cannot represent 0.1 exactly — money belongs in integer cents.
  3. Integers above 2^53 lose precision in JavaScript — pass them as strings.