tooldura

Developer Tools

Where Hex Converters Quietly Round Off Your 64-Bit Numbers

T
tooldura editorial
8 min readUpdated August 22, 2026Open tool →

Convert `FFFFFFFFFFFFFFFF` to decimal and the answer is 18446744073709551615. A great many converters will tell you 18446744073709552000 instead. Nothing failed and nothing warned you; the last four digits were simply spent on a rounding error that happens before the conversion even starts.

Where the Missing Digits Go

JavaScript has one numeric type, and it is a IEEE 754 double-precision float. That is 64 bits, but not 64 bits of integer: 1 for the sign, 11 for the exponent, and 52 stored for the significand, which behaves as 53 because the leading bit is implied.

53 bits of significand means every integer up to 2^53 − 1 is exact and nothing above it is guaranteed to be. That number has a name in the language, Number.MAX_SAFE_INTEGER, and it is 9007199254740991. Past it the representable values start stepping in twos, then fours, and a value that lands between two of them is rounded to whichever is nearer.

So a converter that does parseInt(hex, 16) on a 64-bit value is not converting a 64-bit number. It is converting the nearest double to that number, and then printing that. Nothing in the code looks wrong, which is exactly why the bug survives.

The fix has existed since ES2020: BigInt holds integers of arbitrary size, and BigInt.prototype.toString(base) converts them exactly for any base from 2 to 36. This site's converter uses it throughout, which is why the answer above comes out with all twenty digits intact.

One Value, Read at Four Widths

The bit pattern is identical in every row. Only the declared width and signedness change.

BitsUnsignedSignedHex
8-bit255−1FF
16-bit65535−1FFFF
32-bit4294967295−1FFFFFFFF
64-bit18446744073709551615−1FFFFFFFFFFFFFFFF

Why All Ones Means Minus One

Two's complement is the reason. To negate a number, flip every bit and add one. Do that to 1 in eight bits: 00000001 flips to 11111110, add one and you get 11111111. So −1 is all ones, at every width.

The scheme looks arbitrary until you compare it with the alternatives. Sign-magnitude reserves the top bit for a sign and leaves you with two zeros, positive and negative, which every comparison then has to special-case. Ones' complement has the same problem. Two's complement has exactly one zero.

The bigger win is in the hardware. With two's complement, the circuit that adds two unsigned numbers adds two signed numbers correctly with no changes at all: 11111111 + 00000001 overflows to 00000000, which is right whether you read it as 255 + 1 wrapping to 0 or as −1 + 1 giving 0. One adder, both interpretations. That is why essentially every processor built since the 1960s uses it.

The cost is the asymmetry you meet in edge cases. An 8-bit signed byte runs from −128 to 127, not −127 to 127, because the extra pattern freed up by having only one zero goes to the negative side. It is why Math.abs(-128) on an 8-bit type gives back −128, and why the same thing happens at 32 bits with −2147483648.

🔢

The leading zero that costs eight

In C, and in JavaScript before strict mode outlawed it, a literal starting with a zero is octal. 010 is 8, not 10. This has produced a long tail of bugs in date handling, where 08 and 09 are not even valid octal and would fail outright. Both languages fixed it the same way: an explicit 0o prefix, and an error for the old form. Python 3 made 010 a syntax error for exactly this reason. If you ever pad a numeric string with zeros before parsing it, pass the base explicitly.

Why Hex Won and Octal Did Not

Both exist for the same reason: binary is correct and unreadable. A 32-bit value is thirty-two characters of ones and zeros that no one can compare by eye. Group the bits and you get something a person can hold.

Octal groups them in threes, hexadecimal in fours. That choice used to be a toss-up, because early machines had word sizes divisible by three: the PDP-8 had 12-bit words, four octal digits exactly, and octal was the natural notation for it.

The eight-bit byte settled the argument. A byte is two hex digits, exactly, with no digit straddling a byte boundary. In octal a byte is two and two-thirds digits, so an octal dump has to break alignment somewhere. Once IBM's System/360 fixed the byte at eight bits in 1964 and everyone followed, hexadecimal was the only grouping that lined up with the hardware.

Octal survives in the one place where the grouping happens to be three bits wide: Unix file permissions. chmod 755 is three sets of read, write and execute, one octal digit each. That is not nostalgia, it is the notation fitting the data exactly, which is the same reason hex fits everywhere else.

Convert between every base at once

Binary, octal, decimal, hex and anything up to base 36, exact past 64 bits.

Open Binary & Hex Converter →

The Traps Worth Knowing

Base conversion is arithmetic a child can do. Nearly every bug around it comes from something else.

1

parseInt without a radix

`parseInt("0x1F")` gives 31 because the prefix is honoured, but `parseInt("08")` gave 0 in older engines that read the leading zero as octal. Always pass the base: `parseInt(text, 16)`.

2

Endianness is not base conversion

`0x1234` stored little-endian hits memory as `34 12`. A hex dump shows byte order; a converter shows the value. Reading one as the other reverses your bytes.

3

Leading zeros carry width, not value

`0F` and `F` are the same number, but the first says it is a byte. Strip them from a value and you lose the width; keep them in a comparison and string equality fails on equal numbers.

4

Hex is case-insensitive, checksums are not

`ff` and `FF` are the same value, so compare hex by parsing it or by lowercasing both sides. A raw string comparison of two digests that differ only in case reports a mismatch that is not there.

5

JSON has no integers past 2^53

A 64-bit id in a JSON payload is a double the moment it is parsed, and it comes back rounded. This is why APIs that use snowflake ids send them as strings.

Frequently Asked Questions

Related Tools

Keep Reading