Overflow and Division by Zero
Overflow happens when an arithmetic result does not fit in the type that has to hold it. Division by zero happens when an integer is divided by zero or the remainder is taken with a zero divisor. Rust treats both as bugs: it rejects them at compile time when it can see them, and otherwise it either panics or wraps, depending on the operation and the build profile.
Overflow in Integer Operators
A fixed-size integer has a fixed range. u8 holds 0..=255, so 255 + 1 has no representation in u8. For the operators +, -, *, unary -, and for << and >> with an oversized shift amount, the behavior depends on whether overflow checks are enabled:
| Build | Overflow checks | Behavior on overflow |
|---|---|---|
Debug (cargo build, cargo run) |
On by default | The program panics |
Release (cargo build --release) |
Off by default | The result wraps around (two's complement) |
In a release build the wrapped result is the mathematically correct value modulo 2^N, where N is the bit width of the type. Rust never treats overflow as undefined behavior, unlike C for signed integers.
The program below takes a value at runtime so the compiler cannot see the overflow ahead of time:
fn main() {
let n: i32 = std::hint::black_box(i32::MAX);
println!("{}", n + 1);
}
Debug build:
thread 'main' panicked at src/main.rs:3:20:
attempt to add with overflow
Release build:
-2147483648
i32::MAX + 1 wraps to i32::MIN.
Operations and Their Panic Messages
These are the real messages from a debug build, each produced by an operation on an i32 (or u8 for the last one) obtained at runtime:
| Expression | Panic message |
|---|---|
i32::MAX + 1 |
attempt to add with overflow |
i32::MIN - 1 |
attempt to subtract with overflow |
i32::MAX * 2 |
attempt to multiply with overflow |
-i32::MIN |
attempt to negate with overflow |
1i32 << 32 |
attempt to shift left with overflow |
i32::MIN.abs() |
attempt to negate with overflow |
2i32.pow(40) |
attempt to exponentiate with overflow |
0u8 - 1 |
attempt to subtract with overflow |
The same operations in a release build do not panic. They wrap:
| Expression | Release result |
|---|---|
i32::MAX + 1 |
-2147483648 |
i32::MIN - 1 |
2147483647 |
i32::MAX * 2 |
-2 |
-i32::MIN |
-2147483648 |
1i32 << 32 |
1 (the shift amount is masked to 5 bits, so it shifts by 0) |
i32::MIN.abs() |
-2147483648 |
2i32.pow(40) |
0 |
0u8 - 1 |
255 |
The standard library methods abs, pow, and iterator sum and product follow the same switch as the operators. They panic when the crate that calls them is compiled with overflow checks, and wrap otherwise:
fn main() {
let v: Vec<u8> = vec![200, 100];
let total: u8 = v.iter().sum();
println!("{total}");
}
Debug build:
thread 'main' panicked at /rustc/.../library/core/src/iter/traits/accum.rs:206:1:
attempt to add with overflow
Release build:
44
200 + 100 = 300, and 300 mod 256 = 44.
Changing the Setting
Overflow checks can be switched independently of the optimization level.
With rustc:
rustc -C overflow-checks=on -O main.rs
rustc -C overflow-checks=off main.rs
With Cargo, in Cargo.toml:
[profile.release]
overflow-checks = true
With this setting, cargo run --release produces the panic instead of the wrapped value:
thread 'main' panicked at src/main.rs:3:20:
attempt to add with overflow
Programs that must not silently produce wrong numbers, such as financial or security code, can enable checks in release. The cost is a small runtime check on each arithmetic operation.
Code should not depend on the debug-build panic or on the release-build wrap for correctness. If wrapping is the intent, say so with wrapping_* or Wrapping<T>. If overflow is an error to handle, use checked_*.
Overflow Detected at Compile Time
When the compiler can compute the overflow itself, because every operand is a constant or a value it can trace, it reports an error. The arithmetic_overflow lint is set to deny by default and applies in every build profile.
// ❌ does not compile
fn main() {
let x: i32 = i32::MAX + 1;
println!("{x}");
}
error: this arithmetic operation will overflow
--> c1.rs:3:18
|
3 | let x: i32 = i32::MAX + 1;
| ^^^^^^^^^^^^ attempt to compute `i32::MAX + 1_i32`, which would overflow
|
= note: `#[deny(arithmetic_overflow)]` on by default
The compiler also follows simple variables:
// ❌ does not compile
fn main() {
let x: u8 = 255;
let y = x + 1;
println!("{y}");
}
error: this arithmetic operation will overflow
--> c4.rs:4:13
|
4 | let y = x + 1;
| ^^^^^ attempt to compute `u8::MAX + 1_u8`, which would overflow
|
= note: `#[deny(arithmetic_overflow)]` on by default
Shifts by a constant amount that is too large are caught the same way:
// ❌ does not compile
fn main() {
let x = 1u32 << 32;
println!("{x}");
}
error: this arithmetic operation will overflow
--> c7.rs:3:13
|
3 | let x = 1u32 << 32;
| ^^^^^^^^^^ attempt to shift left by `32_i32`, which would overflow
|
= note: `#[deny(arithmetic_overflow)]` on by default
A literal that does not fit its type is caught by a separate lint, overflowing_literals, which is also deny by default:
// ❌ does not compile
fn main() {
let x: u8 = 256;
println!("{x}");
}
error: literal out of range for `u8`
--> c3.rs:3:17
|
3 | let x: u8 = 256;
| ^^^
|
= note: the literal `256` does not fit into the type `u8` whose range is `0..=255`
= note: `#[deny(overflowing_literals)]` on by default
These checks are best-effort. They catch only what constant analysis can see. Values that arrive at runtime, such as user input, file contents, and function arguments, are checked at runtime.
Division and Remainder by Zero
Integer / and % panic when the right operand is zero. This check is always on. It is not controlled by overflow-checks, so release builds panic too.
fn main() {
let d: i32 = std::hint::black_box(0);
println!("{}", 10 / d);
}
thread 'main' panicked at src/main.rs:3:20:
attempt to divide by zero
fn main() {
let d: i32 = std::hint::black_box(0);
println!("{}", 10 % d);
}
thread 'main' panicked at src/main.rs:3:20:
attempt to calculate the remainder with a divisor of zero
When the compiler can see the zero, the program does not compile. The unconditional_panic lint is deny by default:
// ❌ does not compile
fn main() {
let x = 10 / 0;
println!("{x}");
}
error: this operation will panic at runtime
--> c2.rs:3:13
|
3 | let x = 10 / 0;
| ^^^^^^ attempt to divide `10_i32` by zero
|
= note: `#[deny(unconditional_panic)]` on by default
// ❌ does not compile
fn main() {
let d = 0;
let x = 10 % d;
println!("{x}");
}
error: this operation will panic at runtime
--> c5.rs:4:13
|
4 | let x = 10 % d;
| ^^^^^^ attempt to calculate the remainder of `10_i32` with a divisor of zero
|
= note: `#[deny(unconditional_panic)]` on by default
The MIN / -1 Case
For signed integers, one more input is invalid: dividing the minimum value by -1. The true result, 2147483648 for i32, does not fit in the type. This case panics in both debug and release builds, for both / and %:
| Expression | Panic message |
|---|---|
i32::MIN / -1 |
attempt to divide with overflow |
i32::MIN % -1 |
attempt to calculate the remainder with overflow |
The remainder panics even though the mathematical remainder is 0. When the values are constants, the compiler rejects it:
// ❌ does not compile
fn main() {
let x = i32::MIN / -1;
println!("{x}");
}
error: this operation will panic at runtime
--> c6.rs:3:13
|
3 | let x = i32::MIN / -1;
| ^^^^^^^^^^^^^ attempt to compute `i32::MIN / -1_i32`, which would overflow
|
= note: `#[deny(unconditional_panic)]` on by default
Unsigned integers have no negative values, so only division by zero applies to them.
Floating-Point Types Do Not Panic
f32 and f64 never panic on arithmetic. Overflow and division by zero produce the IEEE 754 special values:
fn main() {
println!("{}", 1.0 / 0.0);
println!("{}", -1.0 / 0.0);
println!("{}", 0.0 / 0.0);
println!("{}", 1.0_f64 % 0.0);
println!("{}", f32::MAX * 2.0);
println!("{}", f64::MAX + f64::MAX);
}
inf
-inf
NaN
NaN
inf
inf
| Situation | Result |
|---|---|
| Nonzero divided by zero | inf or -inf, by sign |
| Zero divided by zero | NaN |
| Remainder with zero divisor | NaN |
| Result larger than the type's maximum | inf or -inf |
NaN is not equal to anything, including itself. Use is_nan(), is_infinite(), or is_finite() to test for these values.
Explicit Overflow Handling
Integer types provide four families of methods that make the overflow behavior part of the code, independent of the build profile.
| Family | On overflow | 250u8 and 10 |
|---|---|---|
checked_* |
Returns None |
250u8.checked_add(10) is None |
wrapping_* |
Wraps around | 250u8.wrapping_add(10) is 4 |
saturating_* |
Clamps to MIN or MAX |
250u8.saturating_add(10) is 255 |
overflowing_* |
Returns (wrapped value, overflowed) |
250u8.overflowing_add(10) is (4, true) |
fn main() {
println!("{:?}", 250u8.checked_add(10));
println!("{}", 250u8.wrapping_add(10));
println!("{}", 250u8.saturating_add(10));
println!("{:?}", 250u8.overflowing_add(10));
}
None
4
255
(4, true)
The families exist for add, sub, mul, div, rem, neg, abs, pow, shl, and shr, with a few gaps: for example, there is no saturating_rem or saturating_shl. Examples with other operations:
fn main() {
println!("{:?}", 200u8.checked_sub(201));
println!("{}", 5u32.saturating_sub(10));
println!("{:?}", 5u32.overflowing_sub(10));
println!("{}", i32::MAX.wrapping_mul(2));
println!("{:?}", 2i32.checked_pow(31));
println!("{}", (-128i8).wrapping_abs());
println!("{:?}", (-128i8).checked_abs());
println!("{}", (-128i8).unsigned_abs());
}
None
0
(4294967291, true)
-2
None
-128
None
128
unsigned_abs returns the unsigned type, which can hold 128, so it cannot overflow.
Handling Division by Zero
checked_div and checked_rem return None for both a zero divisor and the MIN / -1 case, so neither panics:
fn main() {
println!("{:?}", 10i32.checked_div(2));
println!("{:?}", 10i32.checked_div(0));
println!("{:?}", i32::MIN.checked_div(-1));
println!("{:?}", 10i32.checked_rem(0));
}
Some(5)
None
None
None
For input from outside the program, the usual pattern is to test the divisor or use checked_div and turn None into an error:
fn average(values: &[u8]) -> Option<u8> {
if values.is_empty() {
return None;
}
let mut sum: u8 = 0;
for &v in values {
sum = sum.checked_add(v)?;
}
Some(sum / values.len() as u8)
}
fn main() {
println!("{:?}", average(&[10, 20, 30]));
println!("{:?}", average(&[200, 100]));
println!("{:?}", average(&[]));
}
Some(20)
None
None
average(&[200, 100]) overflows u8 while summing, and average(&[]) would divide by zero. Both return None.
The Wrapping and Saturating Types
When all arithmetic on a value should wrap or saturate, std::num::Wrapping<T> and std::num::Saturating<T> apply that behavior to the ordinary operators:
use std::num::{Saturating, Wrapping};
fn main() {
let w = Wrapping(250u8) + Wrapping(10);
println!("{w}");
let s = Saturating(250u8) + Saturating(10);
println!("{s}");
let mut hash = Wrapping(5381u32);
for byte in b"hello" {
hash = hash * Wrapping(33) + Wrapping(*byte as u32);
}
println!("{}", hash.0);
}
4
255
261238937
Hash functions and pseudo-random number generators rely on wrapping arithmetic. Wrapping states that intent, and the code behaves the same in debug and release.
Casts Never Panic
The as operator does not check for overflow. Integer casts truncate to the target width, and float-to-integer casts saturate:
fn main() {
println!("{}", 300u16 as u8);
println!("{}", -1i32 as u32);
println!("{}", 255u8 as i8);
println!("{}", 3.99_f64 as i32);
println!("{}", 1e20_f64 as i32);
println!("{}", f64::NAN as i32);
}
44
4294967295
-1
3
2147483647
0
A float cast truncates the fraction, clamps out-of-range values to the target's MIN or MAX, and turns NaN into 0. When the loss of information would be a bug, use TryFrom/try_from, which returns a Result.
Choosing a Strategy
| Situation | Use |
|---|---|
| Overflow would be a bug, and a crash in debug is acceptable | The plain operator |
| Overflow would be a bug, and release must also catch it | The plain operator with overflow-checks = true |
| Overflow is possible and must be handled as an error | checked_* |
| Wrap-around is the intended math (hashes, counters, cyclic values) | wrapping_* or Wrapping<T> |
| The value should stop at the limit (volumes, progress, clamped counters) | saturating_* or Saturating<T> |
| Both the wrapped result and the overflow flag are needed | overflowing_* |
| Divisor comes from outside the program | checked_div / checked_rem, or test for zero first |