Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

Learn RustChapter 2: Values

Lesson 2.4 · 9 min

How big a number can get

Chatterbox counts your messages. How high can it count before it runs out? In a computer a number lives in a fixed handful of bytes, so every whole-number type has a largest value. Rust makes you pick the size, and it is very clear about what happens at the edge.

Adds to ChatterboxThe counter gets a type that says how big it can grow.

How high can a counter go?

Chatterbox's messages_seen goes up by one for every message. On paper you can always add one more. In a computer you cannot, and the reason is how a number is stored.

Memory is made of bits. A bit is one tiny switch that is either off or on, written 0 or 1. One bit can tell two things apart. Two bits make four patterns (00, 01, 10, 11), three bits make eight, and every extra bit doubles the count. Eight bits together are a byte, the unit memory is measured in, and a byte has 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 = 256 patterns.

A whole number is stored as one of those patterns. So a number kept in one byte can be one of exactly 256 values, and if it is never negative, those are 0 to 255. There is no 257th pattern for 256. Guess what Rust says is the largest number that fits:

PredictThe top of one byte
fn main() {
println!("{}", u8::MAX);
}

u8 is a never-negative whole number stored in one byte, and u8::MAX is the largest one. What does this print?

  • 255
  • 256
  • 127
Reveal the answer

The program prints: 255 One byte has 256 patterns. Counting starts at 0, so they are 0, 1, 2 … up to 255. The 256th value is 255, not 256, in the same way that the 10th digit is 9.

A family of whole numbers

Rust has a whole family of whole-number types, and the name of each one tells you two things. The letter says whether it can be negative: u for unsigned (no minus sign, never negative) and i for integer, which here means one that can be negative too. The number says how many bits it uses. So u8 is 8 bits, never negative; i32 is 32 bits (4 bytes), positive or negative.

A type that allows negatives does not get more patterns, so it shares them out: half below zero and half from zero up. That is why i8 runs from −128 to 127 instead of 0 to 255.

TypeBytesSmallestLargest
u810255
i81−128127
u162065,535
i162−32,76832,767
u32404,294,967,295
i324−2,147,483,6482,147,483,647
u648018,446,744,073,709,551,615
i648−9,223,372,036,854,775,8089,223,372,036,854,775,807
u128160about 3.4 × 1038, a 39-digit number
i12816about −1.7 × 1038about 1.7 × 1038
usize8 on a 64-bit computer0the same as u64 there

The odd one out is usize, which you met in lesson 2.3 as the type for lengths and counts. Its size is not fixed by its name: it is as big as the computer's memory addresses (an address is the number the computer uses to find one spot in its memory, like a house number on a very long street; chapter 4 draws the picture), so 8 bytes on a 64-bit computer (nearly every laptop and phone today) and 4 bytes on a 32-bit one. That is exactly why Rust uses it for lengths and positions: the length of anything in memory can never be bigger than the memory can count. (isize is its negative-allowed twin, and you will rarely need it.)

So which do you pick? i32 is the everyday default, and Rust chooses it for a bare whole number. usize for lengths and positions. A u type for anything that can never be negative, sized to the biggest value you will ever need. Bigger types cost more memory, but only a few bytes each; running out of room costs a lot more. Open each type below: the sizes are measured with this compiler, not copied from a book.

Type InspectorHow many bytes is each number?

every size here is measured with size_of on this compiler

TypeBytesLayout
u81byte 1 (1 B)
i324byte 1 (1 B) · byte 2 (1 B) · byte 3 (1 B) · byte 4 (1 B)
u324byte 1 (1 B) · byte 2 (1 B) · byte 3 (1 B) · byte 4 (1 B)
u648byte 1 (1 B) · byte 2 (1 B) · byte 3 (1 B) · byte 4 (1 B) · byte 5 (1 B) · byte 6 (1 B) · byte 7 (1 B) · byte 8 (1 B)
usize8byte 1 (1 B) · byte 2 (1 B) · byte 3 (1 B) · byte 4 (1 B) · byte 5 (1 B) · byte 6 (1 B) · byte 7 (1 B) · byte 8 (1 B)

Going over the top

So what happens when a u8 already holding 255 is asked to add one more? The true answer, 256, has no pattern. Try it.

PredictOne message too many
fn main() {
let mut messages_seen: u8 = 255;
messages_seen += 1;
println!("messages so far: {}", messages_seen);
}

Does this compile — and if it does, does it run?

  • It compiles and runs
  • It compiles, then panics
  • It does not compile
Reveal the answer

It compiles, then panics at run time. It compiles, then stops at line 3 with attempt to add with overflow. Stopping like this is called a panic: the program halts on purpose, with a message saying where and why, instead of carrying on with a number that is wrong. A build made for trying your code, which is what plain cargo run makes, checks every addition for overflow.

That is what you will see while you work, and it is the useful reaction: the program stops at the exact line, instead of carrying on with a count that is wrong. Rust counts overflow as a mistake. The real fix is a type big enough that it never happens, and for the rare case where the edge is real, the next section shows how to say what you want there.

There are two other reactions, one earlier and one later, folded away below for when you are curious: the compiler refusing an overflow it can see on the page, and a build made for shipping quietly wrapping around to 0.

Going deeper An overflow the compiler can see on the page

When the overflow is right there in the code, with nothing left to find out by running it, the compiler refuses before anything runs. Here is a program that does that, and what cargo build really printed for it:

fn main() {
    let messages_seen: u8 = 255 + 1;
    println!("messages so far: {}", messages_seen);
}
error: this arithmetic operation will overflow
 --> src/main.rs:2:29
  |
2 |     let messages_seen: u8 = 255 + 1;
  |                             ^^^^^^^ attempt to compute `u8::MAX + 1_u8`, which would overflow
  |
  = note: `#[deny(arithmetic_overflow)]` on by default

255 + 1 uses only numbers written in the code, so the compiler works it out itself and sees it cannot fit in a u8. Notice that this message has no E-number. It comes from a lint: an extra check the compiler runs over your code, named in words instead of a number. Most lints only warn, but this one, arithmetic_overflow, is set to refuse by default, which is what the #[deny(arithmetic_overflow)] note means.

Going deeper Debug builds, release builds, and wrapping around

cargo run makes a debug build: quick to compile, full of checks, meant for you while you work (it is the dev profile you saw in lesson 1.2). cargo run --release makes a release build: slower to compile, optimised to run fast, meant for shipping. Checking every single addition costs a little time, so a release build leaves those checks out by default, and the number wraps around instead, like a car's mileage counter rolling from 999999 to 000000: 255 + 1 becomes 0.

Here is a counter that starts at 254 and goes up twice, in src/main.rs of a project called counter:

fn main() {
    let mut messages_seen: u8 = 254;
    messages_seen += 1;
    println!("messages so far: {}", messages_seen);
    messages_seen += 1;
    println!("messages so far: {}", messages_seen);
}

And here it is built both ways, run for real. The debug build stops at line 5, the second addition:

$ cargo run
   Compiling counter v0.1.0 (~/counter)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.36s
     Running `target/debug/counter`
messages so far: 255

thread 'main' (92428561) panicked at src/main.rs:5:5:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

$ cargo run --release
   Compiling counter v0.1.0 (~/counter)
    Finished `release` profile [optimized] target(s) in 0.17s
     Running `target/release/counter`
messages so far: 255
messages so far: 0

Read the second run carefully: a chat that has seen 256 messages proudly says 0. Nothing crashed, which is worse, because nobody notices. The wrap is not a feature you are meant to lean on. Rust counts overflow as a mistake either way; the debug build simply catches it for you.

Saying what you want at the edge

Sometimes the edge is real and expected, and then you should say what you want instead of leaving it to the build. Each whole-number type comes with functions for exactly that. u8::saturating_add(a, b) adds, but stops at the top: past 255 it simply stays at 255. u8::wrapping_add(a, b) wraps around on purpose: past 255 it starts again from 0. A third, checked_add, answers "there is no answer" instead of giving a number; that kind of answer gets its own lesson in chapter 5. You will usually see these written with a dot, as count.saturating_add(10); the dot form arrives in the next lesson, and both mean the same thing.

Stop at the top, or wrap on purpose
fn main() {
let count: u8 = 250;
println!("{}", u8::saturating_add(count, 10));
println!("{}", u8::wrapping_add(count, 10));
}
$ cargo run
255
4

Chatterbox v0.2: a counter with a size

Until now messages_seen had no type written down, so Rust picked i32 for it. That works, but it says the wrong thing: a count of messages can never be negative. The new line makes it a u32: never negative, four bytes, and able to reach 4,294,967,295. Sending one message a second without sleeping, that is more than 136 years of chatting, so the counter will not run out. A u8 would run out after 255 messages, which is one busy afternoon.

The green lines are the type, let mut messages_seen: u32 = 0;, and a comment above it saying why. Nothing else changes: messages_seen += 1; still adds one, and the run still ends with messages so far: 1.

Chatterbox v0.2 after lesson 2.4: the whole program (green lines are new in this lesson)
src/main.rs3 new lines
// Chatterbox: a small chat program, built one lesson at a time.
 
/// The first line Chatterbox prints.
fn banner(bot: &str, version: &str) -> String {
format!("{} v{}", bot, version)
}
 
/// The greeting, with a name filled in.
fn greeting(name: &str, bot: &str) -> String {
format!("Hi {}! I'm {}.", name, bot)
}
 
fn main() {
let name = "Sam";
let bot = "Chatterbox";
let version = "0.2";
// u32: a whole number that is never negative and fits in 4 bytes,
// so it can count past four billion messages before it runs out.
let mut messages_seen: u32 = 0;
let max_len: usize = 200; // the longest message we accept, in bytes
let quiet = false; // true would skip the greeting
 
println!("{}", banner(bot, version));
println!("{}", greeting(name, bot));
println!("Type a message and I will answer.");
println!("(limit: {} bytes a message, quiet: {})", max_len, quiet);
 
// The greeting was our first message, so the count goes up by one.
messages_seen += 1;
println!("messages so far: {}", messages_seen);
}
Chatterbox prints (a real run)
Chatterbox v0.2
Hi Sam! I'm Chatterbox.
Type a message and I will answer.
(limit: 200 bytes a message, quiet: false)
messages so far: 1

Download this version (zip)Unzip, then cargo run inside the folder.

Try itYour turn: a count that sticks at the top

Imagine Chatterbox kept its counter in a single byte, a u8. Make count_up add the new messages to the count without ever panicking: when the total would go past 255, it stays at 255. The starter adds with +, so one of the hidden tests makes it panic. Run it first and read which one.

Imagine Chatterbox kept its counter in a single byte, a u8. Make count_up add the new messages to the count without ever panicking: when the total would go past 255, it stays at 255. The starter adds with +, so one of the hidden tests makes it panic. Run it first and read which one.

fn count_up(count: u8, more: u8) -> u8 {
// add, but never go past 255
count + more
}
 
fn main() {
println!("{}", count_up(250, 10));
}

Hidden tests (5): adding_nothing_changes_nothing adds_normally already_full_stays_full lands_exactly_on_the_top sticks_at_the_top

This exercise runs real Rust in your browser through the official Rust Playground, so it needs JavaScript.

Self-check

Say it out loud, then reveal

Self-checkRetrieval beats re-reading
How many different values fit in 8 bits, and why?Reveal
256. Each bit doubles the number of patterns: 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 = 256.
What do the <code>u</code> and the <code>32</code> in <code>u32</code> tell you?Reveal
u means unsigned, so it is never negative. 32 is how many bits it uses: 4 bytes.
A <code>u8</code> holds 255 and the program, started with plain <code>cargo run</code>, adds 1. What happens?Reveal
It panics with "attempt to add with overflow": it stops at that line instead of carrying on with a wrong number.
Why is Chatterbox's counter a <code>u32</code> and not an <code>i32</code> or a <code>u8</code>?Reveal
A count is never negative, which a u type says. A u8 would run out after 255 messages; a u32 reaches past four billion.

That is lesson 2.4. Tick it off so the course remembers where you are.

Next: 2.5 Text →

Questions people ask

What happens on integer overflow in Rust?

If the compiler can see the overflow in the code itself, it refuses to compile. Otherwise a debug build panics with attempt to add with overflow, and a release build wraps around by default. Rust treats overflow as a bug either way.

What is the difference between u32 and i32 in Rust?

Both use 4 bytes. u32 is unsigned, so it is never negative and goes from 0 to 4,294,967,295. i32 shares its patterns between negative and positive and goes from about minus 2.1 billion to plus 2.1 billion.

How big is usize in Rust?

As big as a memory address on the computer it is built for: 8 bytes on a 64-bit computer and 4 bytes on a 32-bit one. Rust uses it for lengths and positions.

Why does Rust have so many integer types?

Each one trades memory for range. Naming the size lets you say exactly how big a number can get and whether it can be negative, and the compiler checks that every value fits.

What does saturating_add do in Rust?

It adds two numbers but stops at the type's largest value instead of overflowing. For a u8, 250 saturating_add 10 is 255.

Learn Rust by building a chat app · Lesson 2.4 · every compiler message above is real output from rustc 1.99.0 (b940084d7 2026-09-28) (edition 2024) · Vibe Engines · 2026