Coding by Hand
Rust home

Integer Types and Overflow

A number in Rust is an odometer with a fixed set of digit wheels. The cluster behind the steering wheel of a 1970s sedan has six wheels — 999999 miles and the next mile flips it back to 000000. The car does not get smarter when you ask it to count higher. It runs out of wheels. Every integer in Rust works the same way. You pick how many wheels you want — eight, sixteen, thirty-two, sixty-four — and the chip honors that exactly. Add one past the last digit and the number rolls back to the start.

A six-digit mechanical car odometer rolling from 999999 to 000000.
A six-digit mechanical car odometer rolling from 999999 to 000000.

The reason your numbers come in fixed widths is the chip underneath. A CPU register is a row of physical wires soldered into the silicon. Intel shipped the 4004 in 1971 with 4-bit registers because they could only afford 4 wires. The 8008 the next year had 8. The 8086 in 1978 had 16. Each jump cost more transistors and more heat. By the time the Pentium Pro arrived in 1995 the registers were 32 bits wide and the engineers at Intel were arguing about whether 64 was worth the silicon. The point is — no register is infinite. Rust lets you name the width you want and refuses to pretend the wires are longer than they are. The names tell you everything. u8 is an unsigned 8-wire wheel, range 0 to 255. i8 is the signed version, range -128 to 127, because one wire goes to the plus-or-minus sign. u32 and i32 are the 4-byte versions. u64 and i64 are the 8-byte versions. The default integer when you do not pick is i32.

fn print_sizes() {
    println!("u8  holds {} bytes, range {}..={}", 1, u8::MIN, u8::MAX);
    println!("i8  holds {} bytes, range {}..={}", 1, i8::MIN, i8::MAX);
    println!("u32 holds {} bytes, range {}..={}", 4, u32::MIN, u32::MAX);
    println!("i32 holds {} bytes, range {}..={}", 4, i32::MIN, i32::MAX);
}

Now back to the dashboard. A u8 has 256 possible readings, 0 through 255. Set the odometer to 254 and drive one mile. The reading is 255 — the last legal value. Drive another mile. The wheels run out, flip back to 0, and you keep going from there. In Rust this is called wrapping, and you have to ask for it by name with wrapping_add because the language refuses to let it happen by accident in a debug build.

fn odometer_rollover() {
    let mileage: u8 = 254;
    let after_one_mile = mileage.wrapping_add(1);
    let after_two_miles = mileage.wrapping_add(2);
    let after_three_miles = mileage.wrapping_add(3);
    println!("odometer at {mileage}");
    println!("+1 mile -> {after_one_mile}");
    println!("+2 miles -> {after_two_miles}  (rolled over)");
    println!("+3 miles -> {after_three_miles}");
}

Run that and the odometer rolls over right in front of you.

odometer at 254
+1 mile -> 255
+2 miles -> 0  (rolled over)
+3 miles -> 1

The reason Rust makes you spell it out is a rocket. On June 4th 1996 the European Space Agency launched Ariane 5, a 7-billion-Euro booster carrying four science satellites. Thirty-seven seconds into flight, a piece of inertial guidance code from the older Ariane 4 tried to cram a 64-bit floating-point velocity into a 16-bit signed integer. The number was bigger than 32767. The conversion wrapped, the guidance computer read a nonsense angle, the engines swiveled hard, and the rocket tore itself apart in the sky. The cargo was never recovered. The Ariane 5 inquiry board called silent integer conversion the single deadliest default in systems programming, and Rust read that report. In a debug build Rust panics the instant an integer wraps. In a release build it wraps silently to keep the chip fast, but only because you have already promised the compiler you know what you are doing.

How a 64-bit float crammed into a 16-bit signed integer ended the Ariane 5 launch in 1996.
How a 64-bit float crammed into a 16-bit signed integer ended the Ariane 5 launch in 1996.

So Rust gives you three named tools for when overflow might happen and silence would be a disaster. The first is checked_add, which is the dashboard warning light. Instead of returning a number it returns a Some(value) when the math fit and a None when it would have overflowed. Your code looks at the answer and decides what to do.

fn warning_light() {
    let mileage: u8 = 254;
    let safe = mileage.checked_add(1);
    let unsafe_jump = mileage.checked_add(5);
    println!("checked_add(1) -> {safe:?}");
    println!("checked_add(5) -> {unsafe_jump:?}");
}
checked_add(1) -> Some(255)
checked_add(5) -> None

The second tool is saturating_add, which is the fuel gauge. Pour more gas into a full tank and the needle just sits at "F." It does not pretend the tank holds more than it does. saturating_add clamps the result at the maximum, and saturating_sub clamps at zero. Useful when you are counting things like pixels or audio samples where the only sensible answer past the edge is "as far as I can go."

fn fuel_gauge() {
    let tank: u8 = 250;
    let topped_off = tank.saturating_add(20);
    let drained = (5u8).saturating_sub(99);
    println!("saturating_add(20) on 250 -> {topped_off}");
    println!("saturating_sub(99) on 5   -> {drained}");
}
saturating_add(20) on 250 -> 255
saturating_sub(99) on 5   -> 0

The third tool is wrapping_add itself, which you already met. You want it when the rollover is the point — counting clock ticks on a CPU, hashing bytes, anything where modular arithmetic is the right answer. The whole crypto world runs on wrapping integers. Rust gives you all three because the right answer depends on what the number means in your program, and the compiler refuses to guess.

Signed types roll the same way, just with a different ceiling. An i8 tops out at 127. Add one and you do not get 128 — you get -128, because the sign bit flipped. This was the bug that took down Ariane 5, and it is the reason most production Rust code uses unsigned types unless the value really can be negative.

fn signed_rollover() {
    let temp: i8 = 127;
    let next = temp.wrapping_add(1);
    println!("i8 at {temp}, wrapping_add(1) -> {next}");
}

Stack all five together and your terminal prints the whole story — the sizes, the rollover, the warning light, the gauge that caps, and the signed wraparound that put a rocket in the ocean.

u8  holds 1 bytes, range 0..=255
i8  holds 1 bytes, range -128..=127
u32 holds 4 bytes, range 0..=4294967295
i32 holds 4 bytes, range -2147483648..=2147483647

odometer at 254
+1 mile -> 255
+2 miles -> 0  (rolled over)
+3 miles -> 1

checked_add(1) -> Some(255)
checked_add(5) -> None

saturating_add(20) on 250 -> 255
saturating_sub(99) on 5   -> 0

i8 at 127, wrapping_add(1) -> -128
Three dashboard metaphors for Rust's three overflow strategies.
Three dashboard metaphors for Rust's three overflow strategies.

Next lesson — what happens when the number is not whole at all, and why 0.1 + 0.2 does not equal 0.3 on any computer ever built.