Skip to content
Select theme

How to stop an integer from overflowing

Short answer: the + operator panics on overflow in a debug build and wraps around silently in a release build. If an overflow is not supposed to happen, say what should happen instead: checked_add hands you an Option, saturating_add clamps at the type’s limit, wrapping_add wraps on purpose.

From a fresh project — cargo new overflow-demo, then replace src/main.rs with each block below.

Terminal window
cargo run -q
error: this arithmetic operation will overflow
--> src/main.rs:3:15
|
3 | let sum = big + big;
| ^^^^^^^^^ attempt to compute `2000000000_i32 + 2000000000_i32`, which would overflow
|
= note: `#[deny(arithmetic_overflow)]` on by default
error: could not compile `overflow-demo` (bin "overflow-demo") due to 1 previous error

No run, no panic — the program is rejected before it exists. When both operands are visible at compile time, the compiler knows 2_000_000_000 + 2_000_000_000 does not fit in an i32 (whose range stops at 2,147,483,647) and refuses the code.

That check only sees what is written in the file. Real overflow almost always arrives from outside — a number the user typed, a value off the network — and the compiler has no idea what it will be at runtime.

The same addition, with the number passed as a command-line argument (parsing what a user typed is How to parse an integer from user input’s subject):

src/main.rs
fn main() {
let input = std::env::args().nth(1).expect("usage: doubled <number>");
let n: i32 = input.parse().expect("not a number");
println!("{} doubled is {}", n, n + n);
}
Terminal window
cargo run -q -- 2000000000
thread 'main' (9201114) panicked at src/main.rs:4:37:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Exit code 101. The number in parentheses is the thread id, and it is different on every run. The -- matters: everything after it goes to the program, not to cargo.

A debug build — plain cargo run, cargo build, cargo test — has overflow checks on. The program stops at the exact line where the arithmetic went wrong instead of carrying a wrong number forward.

Terminal window
cargo run --release -q -- 2000000000
2000000000 doubled is -294967296

Nothing crashed, and the answer is nonsense. Release builds turn overflow checks off, and the + operator wraps around: 4,000,000,000 modulo 2³² is 4,000,000,000 − 4,294,967,296, which is −294,967,296. That is defined, guaranteed behavior — two’s complement wrap, the same on every platform — not undefined behavior, and not a memory-safety hole. It is just almost never what you meant.

So a release build does not produce a different class of failure than a debug build by accident. The checks are a profile setting, and cargo ships with them on in debug and off in release because the check costs a little time in a loop the optimizer cannot remove it from.

If a wrong number is worse than a slightly slower one, flip the setting rather than hunt every arithmetic line:

Cargo.toml
[profile.release]
overflow-checks = true
Terminal window
cargo run --release -q -- 2000000000
thread 'main' (9191678) panicked at src/main.rs:4:37:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Same panic, same line, same exit 101 — in the build you actually ship. The profile in Cargo.toml only affects your own binary, not the libraries it depends on.

Turning the panic back on is one option. The other is to stop using an operator that can surprise you. checked_add returns None instead of panicking or wrapping, and it does so in every profile:

2000000000 + 2000000000 does not fit in an i32

None here is a value you match on, the same way you match on a failed parse — the overflow becomes an ordinary case in your code instead of a crash the profile decides about.

checked_add has three siblings, and every integer type has the full set. One run shows all four:

None
44
255
(44, true)

200 + 100 does not fit in a u8 (whose maximum is 255), and each variant answers differently. wrapping_add gives 44 — 300 modulo 256, the wrap the release build does by default, written here so it is visible and deliberate. saturating_add clamps at the limit: 255, never more. overflowing_add returns both the wrapped result and whether it wrapped — (44, true) — for the rare caller that wants the value and the fact.

checked_* is the default choice: an overflow means the input or the model was wrong, and None lets the code say so.

wrapping_* is for arithmetic that is modular on purpose. A monotonic counter that stores only 32 bits does not fail when it reaches the top — it starts over, and differences still come out right if you subtract the same way:

11

saturating_* is for counters where the ceiling is an acceptable answer — bytes sent, retries attempted, anything where “at most the maximum” beats a crash or a negative number.

The same families exist for every operation: checked_sub, checked_mul, wrapping_sub, saturating_mul, and the rest, all named the same way.