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.
The compiler catches the obvious case
Section titled “The compiler catches the obvious case”fn main() { let big: i32 = 2_000_000_000; let sum = big + big; println!("{sum}");}cargo run -qerror: 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 errorNo 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.
What slips through: a value from outside
Section titled “What slips through: a value from outside”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):
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);}cargo run -q -- 2000000000thread 'main' (9201114) panicked at src/main.rs:4:37:attempt to add with overflownote: run with `RUST_BACKTRACE=1` environment variable to display a backtraceExit 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.
In release, the same code wraps
Section titled “In release, the same code wraps”cargo run --release -q -- 20000000002000000000 doubled is -294967296Nothing 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.
Keep the checks on in release
Section titled “Keep the checks on in release”If a wrong number is worse than a slightly slower one, flip the setting rather than hunt every arithmetic line:
[profile.release]overflow-checks = truecargo run --release -q -- 2000000000thread 'main' (9191678) panicked at src/main.rs:4:37:attempt to add with overflownote: run with `RUST_BACKTRACE=1` environment variable to display a backtraceSame 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.
Check on purpose: checked_add
Section titled “Check on purpose: checked_add”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:
fn main() { let big: i32 = 2_000_000_000; match big.checked_add(big) { Some(sum) => println!("{big} + {big} = {sum}"), None => println!("{big} + {big} does not fit in an i32"), }}2000000000 + 2000000000 does not fit in an i32None 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.
wrapping, saturating, overflowing
Section titled “wrapping, saturating, overflowing”checked_add has three siblings, and every integer type has the full set. One run shows all four:
fn main() { let big: u8 = 200; println!("{:?}", big.checked_add(100)); println!("{}", big.wrapping_add(100)); println!("{}", big.saturating_add(100)); println!("{:?}", big.overflowing_add(100));}None44255(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.
Which one, when
Section titled “Which one, when”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:
fn main() { let start: u32 = 4_294_967_290; let later: u32 = 5; // the counter wrapped past u32::MAX let elapsed = later.wrapping_sub(start); println!("{elapsed}");}11saturating_* 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.
- How to parse an integer from user input — where the number that overflowed here came from, and how to read it without panicking.
- Choosing the type in the first place —
i32versususize— is the next article’s subject. - Working with numbers — the rest of the chapter.