---
title: "4.4 Integer overflow"
description: "What happens when Rust arithmetic exceeds a type's range: debug panics, release wrapping, and the checked, wrapping, and saturating alternatives."
url: "https://learnrust.net/chapter-4/integer-overflow/"
last_updated: "2026-06-11"
---

# 4.4 Integer overflow

Every integer type has edges, and arithmetic doesn't care: `i32::MAX + 1` is a perfectly grammatical thing to ask for and an impossible thing to store. Exceeding a type's range is called **integer overflow**, this lesson is about Rust's answer to it, and the answer starts with a confession: there are two behaviors, and which one you get depends on the build profile from lesson [0.10](https://learnrust.net/chapter-0/build-configurations/).

## Dev builds: panic

First, a familiar wrinkle from lesson [3.1](https://learnrust.net/chapter-3/syntax-and-semantic-errors/): write the overflow as one constant expression (`let doomed = i32::MAX + 1;`) and Rust refuses to compile it at all, exactly as it refused the provable divide-by-zero. To watch *runtime* overflow behavior, the doomed value has to arrive at runtime, so once again you get to type it personally:

```rust
fn main() {
    println!("How much should we add to i32::MAX?");
    let step = read_number();
    let total = i32::MAX + step;
    println!("{total}");
}

fn read_number() -> i32 {
    let mut input = String::new();
    std::io::stdin()
        .read_line(&mut input)
        .expect("failed to read input");
    input.trim().parse().expect("that wasn't a whole number")
}
```

In a dev-profile build, overflow is checked, and entering `1` fails the check:

```
How much should we add to i32::MAX?
1
thread 'main' (1388359) panicked at src/main.rs:4:17:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```

Named cause, file, line, column: an honest death. During development, the moment your arithmetic escapes its type, you hear about it, at the exact operation responsible.

## Release builds: wrap

Build the same program with `--release`, feed it the same `1`, and it prints:

```
-2147483648
```

No panic. The value **wrapped around**: one past the top of `i32`'s range lands at the bottom, like a car odometer rolling over, because that's what the underlying binary arithmetic naturally does. The release build skips the overflow checks for speed and takes the wrapped result.

Two behaviors sounds alarming, so here's the design logic. The check costs a little time on every single arithmetic operation, a price worth paying while developing (bugs surface instantly) and deliberately skipped in optimized builds (chapter 0.10's whole philosophy). The crucial part, for readers arriving from C++: *both* behaviors are defined. Signed overflow in C++ is undefined behavior, the compiler may assume it cannot happen, and entire optimizations (and exploits) hinge on that assumption. In Rust, overflow is a bug you'll likely catch in development, and a *predictable, boring* wraparound if one survives into release. Boring is the upgrade.

> **Warning**
>
> Don't read "release builds wrap" as "wrapping is fine." Overflow that wraps is still a logic error in waiting (a bank balance of −2,147,483,648 dollars helps no one); the dev-build panic exists to catch it before anyone's balance does. If your program's correctness *depends* on what happens at the edge, say so in code, which is exactly what the next section is for.

## Choosing a policy on purpose

For the cases where the edge is part of the job, every integer type carries method families that make the behavior explicit, whatever the build profile:

```rust
fn main() {
    let max = i32::MAX;
    println!("{:?}", max.checked_add(1));
    println!("{}", max.wrapping_add(1));
    println!("{}", max.saturating_add(1));
}
```

```
None
-2147483648
2147483647
```

`checked_add` returns an "it didn't fit" marker instead of a number when overflow would occur (that `None` is an `Option`, chapter 11's star; the `{:?}` printing trick gets explained in 5.5, consider both previews). `wrapping_add` wraps, on purpose, in writing. `saturating_add` pins at the boundary: the answer is "as big as this type goes." Each has siblings for subtraction, multiplication, and friends.

You won't need these methods often this early. The lesson to keep is their *shape*: where other languages give you one silent behavior and a shrug, Rust's position is that the edge of a type's range is a real situation deserving a real decision, with a default that yells during development and a menu when you want a policy. (This take-a-position pattern will look familiar by chapter 12.)

> **For advanced readers**
>
> The dev/release split is configurable: a one-line profile setting (`overflow-checks = true`) keeps the panics on in release builds, and some teams ship that way, trading a sliver of speed for the louder failure. The default split is a default, not a law.

## Quiz time

**Question #1**

This program is built and run twice: once with `cargo run`, once with `cargo run --release`. Describe what each does.

```rust
fn bump(v: u8) -> u8 {
    v + 1
}

fn main() {
    println!("{}", bump(255));
}
```

<details class="solution">
<summary>Show solution</summary>

Dev: panics inside `bump` with "attempt to add with overflow" (255 is `u8`'s ceiling). Release: wraps around to the bottom of the range and prints `0`. Same program, same bug, different loudness; the dev build is the one doing you the favor.

</details>

**Question #2**

You're implementing a volume knob stored as a `u8`, and turning it past 255 should just *stay* at 255. Which method family says that, and what does the line look like?

<details class="solution">
<summary>Show solution</summary>

Saturating: `volume = volume.saturating_add(step);`. Pinning at the boundary is exactly its meaning, in every build profile, and the method name documents the intent better than any comment.

</details>

**Question #3**

True or false: in Rust, integer overflow is undefined behavior, so anything could happen.

<details class="solution">
<summary>Show solution</summary>

False, and it's a load-bearing false. Overflow is defined in both profiles: panic (dev) or two's-complement wraparound (release). "Anything could happen" is the C++ situation Rust specifically declined to inherit.

</details>

Whole numbers handled, edges and all. Time for the numbers with decimal points, which have a *different* relationship with honesty.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
