Why numbers change when they pass through a program
Numbers look like the safest thing to move between systems, and they are the most common thing to arrive subtly wrong. The reason is that the usual number type is a double — a binary fraction with 53 bits of precision — and the values people care about are decimal fractions and long identifiers, neither of which that format holds exactly. The failures are quiet, because the display rounds the damage away.
Decimal fractions do not exist in binary
One third has no exact decimal form; you write 0.3333 and stop somewhere. In binary the same thing happens to one tenth. The stored value nearest to 0.1 is 0.1000000000000000055511151231257827021181583404541015625, and the nearest to 0.2 is likewise slightly off, so adding them lands on a value that prints as 0.30000000000000004.
This is not a bug in a particular language. Every environment using the same 64-bit format gives the same answer, because the answer is a property of the format. Values that are exact in binary are integers and fractions whose denominator is a power of two, so a half and a quarter behave perfectly while a tenth never will.
Above nine quadrillion, integers stop being consecutive
A double stores integers exactly up to 2 to the 53rd, which is 9007199254740992. One past that cannot be represented at all: ask for 9007199254740993 and you get 9007199254740992 back, silently. Beyond this point the representable integers are spaced two apart, then four, and so on.
That threshold sits right in the middle of a common data type. Sixty-four-bit identifiers — database keys, message and post ids from large platforms, snowflake-style ids — routinely exceed it, so an id that passes through a double comes out as a neighbouring number: still an id-shaped thing, pointing at nothing.
JSON is where this usually happens
JSON itself does not restrict how big a number may be; the format is text and the digits are all there. What loses them is the parser, because most parsers turn every number into a double as they read it. The corruption therefore happens on the receiving side, and the sender's logs look perfect.
The practical rule is to carry large identifiers as strings. An id is not a quantity — you never add two of them — so nothing is lost by quoting it, and quoting is the only thing that survives a round trip through arbitrary parsers. If you cannot change the payload, at least compare the received id against the raw text rather than the parsed number.
Money is the other place it bites
Prices are decimal fractions, so they inherit every problem above: a tenth is not exact, three times a tenth is not three tenths, and a long sum drifts by fractions of a cent that eventually round into a visible discrepancy. Keep money as an integer number of the smallest unit, or in a decimal type made for it.
Rounding is a separate decision that people assume is universal and is not. Rounding a half up, and rounding a half to the nearest even number, both have long histories in accounting, and different languages and spreadsheets pick different defaults — which is how two systems that agree on every input can disagree on a total.
The display hides the damage
Printing a double normally shows the shortest text that reads back as the same value, so a number that is slightly wrong usually prints as the clean value you expected. Two numbers can display identically and still fail an equality test, which is exactly the situation that makes this class of bug feel supernatural.
So compare like with like. To see the real stored value, print it with seventeen significant digits or convert it to a decimal, and to compare two computed values, allow a tolerance scaled to their magnitude rather than testing for exact equality.
Round trips lose digits unless you write enough of them
Converting a double to text and back is exact only if the text carries seventeen significant digits; for a 32-bit float the figure is nine. Anything that formats to six decimal places for readability — a log line, a CSV export, a config file — has thrown away part of the value, and reading it back gives a different number than the one you started with.
The same applies to changing precision on purpose. A 32-bit float holds about seven significant digits, so pushing a double through one and back leaves 0.1 as 0.100000001490116119384765625. That is fine for a coordinate in a game and not fine for a measurement you will later sum a million times.
What to check when a number looks wrong
Test the boundaries deliberately rather than waiting to be surprised. Send 0.1 plus 0.2 through the path and see whether the extra digits appear; send 9007199254740993 and see whether it comes back unchanged; send a value with more than seventeen significant digits and see what survives.
When something is off, work out which step lost the information: the display, the parser, a float conversion, or the arithmetic itself. Each has a different fix — print more digits, quote the id, use a wider type, or move to integers — and applying the wrong one leaves the problem in place while making it harder to see.