A let mut counter with a small integer type has a hard ceiling. Increment past it under cargo run and the program panics immediately with "attempt to add with overflow" — which feels like a safety net. Ship the exact same code as cargo build --release, and that safety net is gone: release profiles disable overflow checks by default, so the same increment silently wraps back to the type's minimum instead of panicking, and the program keeps running with a wrong number nobody flagged.
fn main() {
let mut retries: u8 = 255;
retries += 1;
println!("retries: {}", retries);
}
// cargo run: thread 'main' panicked at src/main.rs:3:5: attempt to add with overflow
// cargo run --release: retries: 0A variable declared with let is immutable by default — attempting to reassign it is a compile error, not a runtime warning. Writing let mut explicitly opts into mutability for that one variable. This default is deliberate: most variables in a well-structured program never need to change, and making mutation opt-in surfaces exactly which parts of the code do change state, which helps both the borrow checker and human readers reason about the program.
let vs. let mut
Reassigning a plain let variable is a compile error, caught before the program ever runs:
let x = 5;
// x = 6; // COMPILE ERROR: cannot assign twice to immutable variable
let mut y = 5;
y = 6; // fine — y was declared mutable
println!("{}", y); // 6That same mut keyword is also where the overflow trap above hides: retries += 1 is a plain mutation, and whether it panics or wraps depends entirely on which profile compiled it. The fix is to stop depending on that profile difference at all — call an explicit, profile-independent method that states what should happen on overflow, such as saturating_add, which caps at the type's maximum instead of panicking or wrapping:
fn main() {
let mut retries: u8 = 255;
retries = retries.saturating_add(1);
println!("retries: {}", retries);
}
// cargo run and cargo run --release both print: retries: 255Constants: always immutable, known at compile time
const declares a value that must be known at compile time and can never be mutable, even with mut — it's a distinct concept from an immutable let variable, which can still hold a value computed at runtime.
const MAX_RETRIES: u32 = 3; // must be a compile-time constant, type annotation requiredShadowing: a new variable, not a mutation
Declaring a new variable with let using a name that already exists doesn't mutate the old one — it creates a brand-new variable that happens to reuse the name, optionally with a different type. This is different from mut: the old value still can't be changed, and shadowing can even change the variable's type, which reassignment through mut could never do.
let spaces = " "; // spaces is a &str
let spaces = spaces.len(); // spaces is now a usize — a NEW variable, different type
println!("{}", spaces); // 3