A for loop's range bound looks harmless when it's computed with ordinary subtraction, like "all indices except the last" via len() - 1. The moment the collection is empty, that subtraction underflows — 0 - 1 on an unsigned usize has no negative result to fall back to. Debug builds catch it with a panic. Release builds don't: the subtraction silently wraps around to the type's maximum value, turning a loop meant to run zero times into one that tries to run billions of times instead.
fn main() {
let items: Vec<i32> = vec![];
for i in 0..(items.len() - 1) {
println!("{}", i);
}
println!("done");
}
// debug: thread 'main' panicked at src/main.rs:3:17: attempt to subtract with overflow
// release: items.len() - 1 wraps to 18446744073709551615 (usize::MAX) -- the loop
// never reaches 'done', it just keeps countingRust’s if is an expression, not just a statement — it can produce a value usable directly in a let binding, provided every branch produces the same type. Rust has three loop keywords: loop (infinite, until an explicit break), while (condition-based), and for (iterating a range or collection) — and uniquely, break inside a loop can carry a value out of the loop entirely.
if as an expression
Because if/else is an expression, it can be assigned directly — but every branch must evaluate to the same type, or it's a compile error.
let condition = true;
let number = if condition { 5 } else { 6 }; // if is an EXPRESSION here
println!("{}", number); // 5
// let bad = if condition { 5 } else { "six" }; // COMPILE ERROR: mismatched typesThree loops: loop, while, for
for is idiomatic for iterating a known range or collection; while loops on a condition; loop repeats forever until an explicit break.
for i in 0..5 { // idiomatic: range-based, exclusive of 5
println!("{}", i);
}
let mut n = 0;
while n < 3 { // condition-based
n += 1;
}
loop { // infinite until break
println!("runs once");
break;
}A for loop's range is only as safe as the arithmetic that built it — that's the trap in the bug above. items.len() - 1 assumes there's at least one element; on an empty Vec, the subtraction underflows before the loop even starts. saturating_sub removes the assumption instead of hoping it always holds, clamping to 0 rather than wrapping around:
fn main() {
let items: Vec<i32> = vec![];
for i in 0..(items.len().saturating_sub(1)) {
println!("{}", i);
}
println!("done");
}
// donebreak can return a value from loop
This is a genuinely unusual feature: break inside a loop (only loop, not while or for) can carry a value, making the whole loop itself usable as an expression — a natural fit for retry logic that needs the successful result.
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // exits the loop AND produces a value
}
};
println!("{}", result); // 20