← Debugging Guides

阅读中文版本

Rust — Integer Overflow Panics in Debug, Wraps in Release

Explain why attempt to add with overflow panics in debug builds but wraps silently in release, make overflow strategy explicit, and check both profiles in CI.

beginnerdebuggingrustintegersbuild-profiles

Symptoms

  • thread panicked at attempt to add with overflow in debug or test builds, absent in release builds
  • Values behave inconsistently between cargo test and cargo build --release: counts go negative or wildly wrong in release
  • The divergence appears only after a value crosses the type maximum (for example u32 past 4294967295)

Possible causes

  • Debug builds compile arithmetic with overflow checks that panic; release builds default to wrapping
  • A counter, checksum, or index computation exceeds the type range under production data volume
  • Tests run on debug profiles catch the panic, but the shipped release wraps silently and corrupts downstream logic
  • Casting between types (i64 to u32) truncates before any overflow check can fire

Diagnosis steps

Reproduce in both profiles

cargo test overflow && cargo build --release && ./target/release/app --stress-input

Interpretation

A panic in debug and silently wrong values in release for the same input is the exact divergence this error class describes.

Locate the overflowing expression

RUST_BACKTRACE=1 cargo test overflow 2>&1 | grep -A6 "with overflow"

Interpretation

The debug panic names the operation and line precisely — that expression needs an explicit strategy.

Check for truncating casts nearby

grep -Rn "as u32\| as u16\| as i32" src/ | head -10

Interpretation

as-casts truncate silently in both profiles and often create the overflow condition before the arithmetic even runs.

Root cause

Rust defines arithmetic overflow behavior per build profile: debug builds insert checks that panic (attempt to add with overflow), while release builds default to two-complement wrapping for performance. The same exceeding computation therefore crashes in development and silently corrupts values in production.

Fix

Choose overflow behavior explicitly: checked_* with error propagation when overflow is possible, saturating_* for clamps, wrapping_* when wraparound is the algorithm (hashes, checksums), and keep overflow checks on in release for untrusted-input paths. Run tests against the release profile in CI.

Risk note

Globally enabling overflow-checks in release adds hot-loop cost. Scope it to builds that process untrusted input, or pay the cost knowingly.

Verify the fix

Run the overflow-triggering workload in debug and release with the chosen explicit operators and assert identical, defined behavior in both profiles.

Symptom walkthrough

The same input, two opposite behaviors:

$ cargo test
thread 'stat::tests::total' panicked at 'attempt to add with overflow',
src/stat.rs:44:25

$ cargo run --release -- --stress
total: -4294967295   <- silently wrapped

This is documented, deliberate behavior: debug profiles compile arithmetic with runtime overflow checks that panic, release profiles compile to two-complement wraparound for speed. The panic in development is the diagnostic gift — it names the exact expression. The release wrap is the dangerous twin: no signal, wrong values, corrupted downstream decisions. Casts (value as u32) truncate in both profiles before any check fires, and are often the real culprit upstream.

Fix in practice

Make the strategy explicit at the site:

// overflow is possible and meaningful — propagate:
let total = a.checked_add(b).ok_or(StatError::Overflow)?;

// clamp — counters, gauges, percentages:
let used = used.saturating_add(delta);

// wraparound IS the algorithm — hashes, checksums:
let hash = (hash.wrapping_mul(31)).wrapping_add(byte as u32);

And align CI with production reality — test the release profile too:

cargo test --release

For services handling untrusted input where any overflow is a bug, keep the checks on in release:

[profile.release]
overflow-checks = true