6.4 loop: Infinite Loops with break-With-Value
Right, so you’ve met loop. It looks a bit like a sad, forgotten while true { }, but that’s because you haven’t seen its party trick: break doesn’t just stop the loop; it can hand you a value. This turns loop from a simple control flow construct into Rust’s primary way of expressing “try this until it works, and when it does, give me the result.” It’s the workhorse for retry logic, parsing, and any situation where success is guaranteed… eventually.
Think of it like this: a while true loop runs forever and returns (). A loop expression runs until you break, and whatever value you tag onto that break becomes the value of the entire loop block. This is why it’s a fundamental expression in Rust, not just a statement.
The break-with-value mechanic
Here’s the simplest possible example. It’s stupid, but it illustrates the syntax.
let result = loop {
// ... do some work ...
if some_condition {
break 42; // This breaks the loop AND makes the whole loop equal to 42
}
// If we don't break, we just go around again.
};
println!("The result is: {}", result); // Prints "The result is: 42"
The magic is that the semicolon after the break expression is optional in this case. The compiler understands that the value of the block is being determined. This is one of those places where Rust’s expression-oriented nature shines.
A realistic example: retrying an operation
Let’s say you’re trying to read input from a user until they give you a valid integer. This is a classic use case. A while loop would be clunky because you’d have to declare your variable beforehand and then assign to it inside the loop. With loop and break-with-value, it’s clean and contained.
use std::io;
let user_number: i32 = loop {
println!("Please enter a number:");
let mut input = String::new();
io::stdin().read_line(&mut input).expect("Failed to read line");
match input.trim().parse() {
Ok(num) => break num, // Break out of the loop, returning `num`
Err(_) => println!("That's not a valid number. Try again."),
};
// The loop continues automatically if we didn't break
};
println!("You entered: {}", user_number);
See how elegant that is? The entire operation—prompting, reading, parsing, and error handling—is wrapped up into a single expression that assigns directly to user_number. The scope is tight, and there’s no need for a mutable variable floating around in an outer scope. This is how you’re meant to do it.
The type consistency requirement
Here’s where the compiler becomes your brilliantly pedantic friend. Every break expression in a loop must evaluate to the same type. Why? Because the compiler needs to know the concrete type of the loop expression at compile time. It can’t wait around to see which branch you take at runtime.
This code will spectacularly fail to compile:
let result = loop {
if some_condition {
break 10; // This is an i32...
} else if some_other_condition {
break "done"; // ...and this is a &str. Nope.
}
// The compiler throws its hands up. "What is the type of `result`?!" it screams.
};
The error message is actually very clear: “break expressions must be of the same type.” It will point to the first break and say “this is an integer” and then point to the second and say “this is a string literal, you maniac.” It’s a very reasonable demand.
Using it with loop labels
When you break from a nested loop, you usually just want to break the innermost one. But if you need to break all the way out of an outer loop and return a value, you use a label. It looks a bit gnarly but it’s incredibly effective.
'outer: loop {
println!("Outer loop.");
loop {
println!("Inner loop.");
// This only breaks the inner loop: break;
// This breaks the outer loop with a value:
break 'outer "I'm outta here!";
}
// This line won't be reached
};
// The expression now evaluates to the string "I'm outta here!"
The label (like 'outer:) lives on the loop you want to break from. The break then specifies that label. It’s explicit, a bit verbose, and completely unambiguous. I’ll take that over a mysterious break that somehow jumps three levels any day.
A common pitfall: forgetting the value
The most common mistake is forgetting that loop is an expression that expects a value. If you have a loop that doesn’t break with a value, or if a particular code path could fail to break, the loop’s type is inferred as ! (the never type), because it conceptually runs forever. This is fine until you try to assign it to something.
// This is fine. The compiler knows this loop never returns.
let _ = loop {
println!("This runs forever...");
};
// This will NOT compile. The type of `x` can't be determined.
// The compiler complains: "the `loop` loops forever and doesn't break"
let x = loop {};
If your loop is supposed to break with a value, make sure every logical path does break with a value. The compiler will check this for you, but it’s good to keep in your own head.