A fixed-width integer has no room above its maximum, so adding one to it produces the minimum instead. An unsigned eight-bit counter goes from 255 to 0; a signed one goes from 127 to −128. No exception is raised and no flag reaches the program by default, which is what makes overflow a class of bug rather than an error: the arithmetic is silently wrong and everything downstream believes it.
The encoding that makes this happen is two’s complement, and it is not a flaw in it. The wrap is the direct consequence of the property that makes the encoding worth having, which is that addition and subtraction need only one circuit.
What does the wrap actually do?
It discards the bits that did not fit. Addition proceeds normally, the result needs one more bit than the type has, and that bit is dropped — which in modular terms means the arithmetic is done modulo 2 to the power of the width.
| Width | Unsigned range | Signed range |
|---|---|---|
| 8-bit | 0 to 255 | −128 to 127 |
| 16-bit | 0 to 65,535 | −32,768 to 32,767 |
| 32-bit | 0 to 4,294,967,295 | −2,147,483,648 to 2,147,483,647 |
| 64-bit | 0 to about 1.8 × 10^19 | about −9.2 × 10^18 to 9.2 × 10^18 |
The signed rows are the ones worth memorising, because 2,147,483,647 turns up in the wild constantly. It is the largest value a 32-bit signed integer holds, and a surprising amount of software still stores counts, identifiers and timestamps in one.
Why is signed overflow different from unsigned?
In C and C++ the two are not merely different in result, they are different in status. Unsigned overflow is defined to wrap, and a program may rely on it. Signed overflow is undefined behaviour, which means the compiler is entitled to assume it never happens.
That assumption has teeth. A check written as if (x + 1 < x) to detect overflow can be removed entirely by an optimiser, on the reasoning that signed overflow cannot occur and therefore the condition is always false. The test disappears, the build is correct by the standard, and the protection is gone.
The reliable form tests the operands rather than the result — comparing against the type maximum before adding — or uses the builtin overflow-checked functions the major compilers provide. Rust checks in debug builds and wraps in release unless asked otherwise, and Python sidesteps the question by promoting to arbitrary precision.
Where is the asymmetric range a trap?
At the one value with no positive counterpart. Negating the most negative number cannot produce anything representable, so it produces itself: the negation of −2,147,483,648 in 32 bits is −2,147,483,648.
This breaks the assumption that an absolute value is never negative, which is exactly the kind of thing a sanity check relies on. A routine that takes the magnitude of a difference and expects a non-negative result gets a negative one for a single input, and then indexes an array with it.
Division has the same edge. Dividing the minimum by −1 overflows for the identical reason, and on several processors it raises a hardware exception rather than wrapping.
Which real failures were this?
The well-documented ones are worth knowing because they were all cheap to prevent and expensive to have. Ariane 5 flight 501 lost the vehicle when a 64-bit floating-point value was converted into a 16-bit signed integer that could not hold it, in code carried over from a slower rocket where the value had never got that large.
A Boeing 787 airworthiness directive required a periodic power cycle because a counter of hundredths of a second in a 32-bit signed integer overflowed after 248 days, putting the generator control units into a failsafe state simultaneously.
The year 2038 problem is the same shape on a schedule everyone can see: a signed 32-bit count of seconds since 1970 runs out in January 2038. The fix is a wider type, and the work is finding every place the narrow one was stored.
How do you catch one before it ships?
By making the machine complain. Sanitisers built into GCC and Clang trap signed overflow at runtime, and turning one on for a test suite is usually a day of work and a permanent result.
Choosing the type deliberately is the other half. A count that could plausibly exceed two billion belongs in a 64-bit type from the start, and an identifier that will be stored, transmitted and parsed by other systems belongs in one whatever the current row count suggests.
Where a value genuinely must saturate rather than wrap — audio samples, pixel channels — that behaviour has to be written explicitly, because no integer type provides it for free.
Questions people ask
Does the processor know it happened? Yes. Hardware sets an overflow flag on the arithmetic, and the reason your program does not see it is that most languages do not check it after every operation.
Is wrapping ever wanted? Frequently. Hashes, checksums and cyclic sequence numbers all rely on modular arithmetic, which is why the unsigned behaviour is defined rather than left open.
Do 64-bit types end the problem? For counters and timestamps, effectively — 64 bits of seconds outlasts any system being written now. For products and accumulations it does not, because multiplying two large values overflows a 64-bit type as readily as a 32-bit one.
What about JavaScript? Numbers are doubles, so ordinary arithmetic does not wrap; it loses precision above 2^53 instead, which is quieter still. The bitwise operators are the exception, and they convert to 32-bit signed first.
The wrap is arithmetic doing exactly what the encoding says, on a value nobody expected to reach the edge. The two’s complement calculator shows what a value looks like at each width and where its boundaries sit, and the number base converter is the faster way to see what the bits did when a result arrives that makes no sense in decimal.