The loop that returns a value
You're building a network client. The server is flaky. You need to retry a request until it succeeds or you hit a max attempt limit. Your Python muscle memory screams while True:. You type it in Rust. The code compiles. It works. But your linter complains, and a Rustacean reviewer suggests loop. Why does Rust have two ways to spin forever, and why does one of them let you return a value from the loop itself?
Rust provides loop as a first-class control flow construct for unbounded repetition. while true is a conditional loop where the condition is hardcoded to be true. They look similar, but the compiler treats them differently. loop is an expression that can produce a value. while is a statement that always produces (). This distinction shapes how you write idiomatic Rust and eliminates a class of boilerplate code.
Concept: Expression vs Statement
In Rust, expressions produce a value. Statements do not. This rule applies to control flow.
A while loop is a statement. It runs side effects and then yields (). You cannot assign a while loop to a variable. You cannot return a while loop from a function as the result. The loop is a command: "Do this while the condition holds."
A loop is an expression. It runs side effects and yields the value passed to break. You can assign a loop to a variable. You can return a loop from a function. The loop is a machine: "Run until told to stop, then hand me the result."
This difference matters when you need to compute a value through repetition. In other languages, you declare a mutable variable outside the loop, update it inside, and return it after the loop exits. In Rust, you can often skip the variable entirely. The loop itself becomes the value.
fn main() {
// loop is an expression. The break value becomes the loop's value.
let result = loop {
println!("Checking condition...");
// Simulate success condition
break 42;
};
println!("Got: {}", result);
// while true is a statement. It cannot return a value.
// Assigning this to a variable would be a type error.
while true {
println!("Spinning...");
break;
}
}
The loop block evaluates to 42. The while block evaluates to (). If you try to assign the while loop to a variable expecting an integer, the compiler rejects you with E0308 (mismatched types). The compiler sees () on the right side and an i32 expectation on the left.
Convention: Clippy and idiomatic code
The Rust community treats loop as the standard for infinite repetition. while true is considered a code smell. It signals that the author is translating patterns from C, Python, or JavaScript rather than using Rust's native constructs.
Clippy, the official linter, includes a lint called while_true. It triggers whenever you write while true and suggests replacing it with loop. The suggestion is not just stylistic. It reminds you that loop offers features while true does not, such as value returns and clearer intent.
Follow the lint. If you see while true in a codebase, rewrite it to loop. The change takes seconds and aligns your code with community expectations.
Realistic example: Retry with backoff
Consider a function that polls a resource. It retries up to three times with increasing delays. It returns the success value or an error string. Using loop with a break value eliminates the need for an intermediate Option or Result variable.
/// Polls a resource with retries and returns the outcome.
///
/// Returns the success value on the first success, or an error message
/// if all attempts fail.
fn poll_resource() -> String {
let mut attempts = 0;
loop {
attempts += 1;
// Check attempt limit before doing work
if attempts > 3 {
break "Failed after 3 attempts".to_string();
}
// Simulate fetching data
// In real code, this would be an async call or blocking I/O
let data = fetch_data(attempts);
if data.is_some() {
// Return the data immediately
break data.unwrap();
}
// Simulate delay between retries
// std::thread::sleep(std::time::Duration::from_millis(100 * attempts));
}
}
fn fetch_data(_attempt: u32) -> Option<String> {
// Simulate success on attempt 2
None
}
The loop returns a String. The break expressions provide the value. One branch returns the error message. The other returns the fetched data. The function body is clean. There is no let mut result = None; declaration. There is no result.unwrap() at the end. The control flow and the value flow are unified.
If you tried to write this with while true, you would need a mutable variable to hold the result. You would update the variable inside the loop and return it after the loop. The loop approach is more direct and less prone to initialization errors.
Walk-through: Type inference and break
The compiler infers the type of a loop from its break expressions. All break expressions in a single loop must have the same type. If they do not, the compiler rejects the code.
fn main() {
let value = loop {
// This branch breaks with an i32
if true {
break 10;
}
// This branch breaks with a String
// The compiler rejects this with E0308 (mismatched types)
// because the loop must have a single type.
break "hello".to_string();
};
}
The compiler analyzes all paths through the loop. It sees break 10 and break "hello".to_string(). It expects a single type for the loop expression. It finds two different types. It emits E0308. You must ensure every break provides a value of the same type.
You can also use break without a value. A bare break provides (). If the loop is assigned to a variable, the variable must be of type (). If the loop is used in a context expecting a different type, a bare break causes a type error.
fn main() {
// This loop has type ()
let _ = loop {
println!("Running");
break;
};
// This causes E0308 because the loop is used where i32 is expected
// but break provides ()
let x: i32 = loop {
println!("Running");
break;
};
}
The compiler enforces consistency. This prevents subtle bugs where one exit path returns a value and another returns nothing. The type system catches the mismatch at compile time.
Labels and nested control flow
Both loop and while support labels. Labels let you break or continue specific loops in nested structures. This is useful when you have multiple loops and need to exit the outer one from the inner one.
fn main() {
'outer: loop {
println!("Outer loop iteration");
loop {
println!("Inner loop iteration");
// Break only the inner loop
break;
}
println!("Back in outer loop");
// Break the outer loop using the label
break 'outer;
}
println!("Exited both loops");
}
The label 'outer marks the outer loop. break 'outer exits the outer loop directly. break without a label exits the innermost loop. This gives you precise control over flow in complex nested structures.
Labels work with continue as well. continue 'outer skips the rest of the outer loop body and starts the next iteration of the outer loop. This is handy when you need to restart an outer process based on inner state.
Labels are not limited to loop. You can label while and for loops too. However, loop is the idiomatic choice for infinite loops, so labels appear most often on loop blocks in practice.
Optimization: AST vs LLVM IR
Developers sometimes ask whether while true generates a check every iteration. The answer depends on the compilation stage.
In the Abstract Syntax Tree (AST), while true has a condition. The compiler sees a boolean expression that is always true. In the LLVM Intermediate Representation (IR), the optimizer removes the check. The generated machine code for while true and loop is identical when the body is the same. The optimizer recognizes the constant condition and eliminates the branch.
The difference is in the AST and the type system. The AST difference matters for lints, for type inference, and for the break value feature. The IR difference does not matter for performance. Both forms compile to the same efficient code.
Choose loop for the type system benefits and idiomatic clarity, not for performance. The performance is the same. The semantics are better.
Pitfalls and compiler errors
Using loop introduces specific pitfalls. The compiler catches most of them, but understanding the errors helps you fix them quickly.
Infinite loops without break. A loop without a break runs forever. The compiler does not always warn about this. If you forget a break in a conditional branch, your program hangs. Use break explicitly in every exit path. If a branch can exit the loop, it must contain a break.
Type mismatch on break. If you assign a loop to a variable, every break must provide a value of the correct type. If you mix types, you get E0308. If you use a bare break when a value is expected, you get E0308. The compiler tells you the expected type and the found type. Align the break expressions with the expected type.
Unreachable break. If a break appears after a return or a panic, it is unreachable. The compiler warns about unreachable code. Remove dead break statements to keep the code clean.
Label scope errors. Labels must be in scope. If you reference a label that does not exist, the compiler rejects the code. If you use a label on the wrong loop type, the compiler rejects the code. Labels are tied to the loop they mark. Use them carefully.
Treat loop as a value-producing machine. If you aren't returning a value, ask yourself if you really need a loop or if a for loop would be safer.
Decision: when to use loop vs alternatives
Use loop when you need unbounded repetition until a condition is met inside the body. Use loop when the loop must return a value via break to eliminate intermediate variables. Use while condition when the loop depends on a boolean check evaluated before each iteration. Use for item in iterator when you are processing a sequence with a defined end. Avoid while true in idiomatic Rust; it triggers Clippy lints and signals a translation from another language rather than a deliberate Rust design choice.