In Rust, integer overflow panics in debug builds, but a release build with --release does not include those checks and wraps around using two's complement. A bug you never saw in testing can therefore behave differently in production. Use the explicit method families so the behavior is a decision, not an accident.
What the Rust Book says
When compiling in debug mode, Rust includes checks for integer overflow that make the program panic at runtime. With the --release flag, those checks are not included. If overflow occurs, Rust performs two's complement wrapping: for a u8, 256 becomes 0 and 257 becomes 1. The program does not panic, but the value is probably not what you expected. The book states that relying on wrapping behavior is considered an error.
Why this matters for security
Memory safe code can still contain logic bugs. An arithmetic overflow in a size calculation, a price, a balance, a quota or a loop bound can lead to wrong decisions even when no memory is corrupted. For example, a length computed with wrapping math might pass a bounds check and then be used to allocate a smaller buffer than the data needs. In safe Rust the later access still panics rather than corrupting memory, but in code that uses unsafe blocks or FFI, a wrong size can become a memory safety issue.
Tests usually run in debug mode, so overflow panics show up there only if the test inputs reach the edge. Production runs in release mode, where the same input silently wraps.
The four method families
The book names these for primitive numeric types:
wrapping_*wraps in all compilation modes, such aswrapping_add.checked_*returnsNoneif there is overflow.overflowing_*returns the value and a boolean saying whether overflow happened.saturating_*stops at the minimum or maximum value of the type.
fn main() {
let x: u8 = 255;
println!("{}", x.wrapping_add(1)); // 0
println!("{:?}", x.checked_add(1)); // None
println!("{:?}", x.overflowing_add(1)); // (0, true)
println!("{}", x.saturating_add(1)); // 255
}
Which to use where
- Sizes, lengths, offsets, money:
checked_*, and treatNoneas an error. - Hashes and checksums where wrap is the point:
wrapping_*, so intent is visible. - Counters and gauges for display:
saturating_*. - Need to know and continue:
overflowing_*.
Making release builds stricter
Cargo lets you set overflow-checks = true in a release profile if you prefer a panic over a silent wrap, at some performance cost. Check the Cargo documentation for the current setting name and measure the cost for your workload before adopting it.
Review checklist
- Arithmetic on values that come from input, files or the network uses
checked_*. - Casts with
asbetween integer types are reviewed, since they can truncate. - Tests cover maximum and minimum values.
- Release profile settings are a deliberate choice.
Safeguard's dependency scanning helps you track advisories in the crates your project uses, and a free scan is available at gold.safeguard.sh/scan.
Quick check
Does Rust always panic on overflow?
No. Debug builds panic. Release builds built with --release wrap unless you change the profile.
Which method should I use for a buffer size?
checked_*, and return an error if the result is None.
Sources
Last reviewed: 19 October 2026.