Loops

A deep guide to Rusts loop constructs - loop, while, for - covering expression-based returns, labels, common pitfalls, and how each loop fits into real-world Rust code

The loop Keyword

Rust’s loop is an infinite loop that can optionally return a value—a feature no other loop construct in the language offers. It is the simplest loop: no condition to check, no iterator to advance, just a block of code that repeats forever until you explicitly stop it.

fn main() {
    loop {
        println!("This will print forever unless we break.");
        // Without a break, the program will never leave this loop.
    }
}

When the compiler sees loop, it knows the block will never exit on its own. That knowledge enables two useful things: the compiler can optimize the loop more aggressively, and—more importantly for you—the loop can produce a value through a break statement.

Breaking Out with a Value

Inside a loop, you can write break expression; to exit the loop and evaluate to expression. This turns loop into an expression that can be assigned to a variable.

fn main() {
    let mut counter = 0;
    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2; // exit the loop and return this value
        }
    };
    println!("The result is: {}", result); // 20
}

The value after break can be any expression—a number, a string, a struct, or even a function call. If no value is written (just break;), the loop evaluates to the unit type ().

Only loop can return a value:

The break value syntax is exclusive to loop. You cannot use break value inside a while or for loop—trying to do so will cause a compile error. If you need to produce a result from a condition‑checked loop, you must use a mutable variable declared outside the loop.

How Beginners Should Think About loop

Imagine you need to keep doing something until a particular condition arises, and once that condition is met you want to hand back a result. loop is the block that says “keep going” and break is the point where you say “stop—and here’s the answer.” This pattern replaces the “infinite loop with a flag” that often appears in other languages.

Always ensure a break exists—or the loop is truly intended to run forever:

A loop without any break will hang your program. The compiler will not warn you about this because infinite loops are sometimes intentional (server accept loops, embedded device supervisors). When you write loop, double‑check that some code path inside it eventually hits a break or calls return from the enclosing function.

Conditional Looping with while

When you need a loop that checks a condition before each iteration and you do not need to return a value from the loop body, while is the right tool. It keeps executing the block as long as the condition evaluates to true.

fn main() {
    let mut countdown = 5;
    while countdown > 0 {
        println!("{}!", countdown);
        countdown -= 1;
    }
    println!("Liftoff!");
}

The condition is evaluated at the start of every iteration. If the condition is false the first time, the body never runs. This makes while an entry‑controlled loop, exactly like its counterparts in C, JavaScript, and Python.

while Always Returns Unit

Unlike loop, a while loop is a statement—not an expression that can carry a value. It always evaluates to () (the unit type). If you need to compute a result while looping with a condition, you must use a variable that the loop updates and that the surrounding code can read afterward.

fn first_multiple_of(m: u32, limit: u32) -> Option<u32> {
    let mut candidate = m;
    while candidate < limit {
        if candidate % 13 == 0 {
            return Some(candidate);
        }
        candidate += m;
    }
    None
}

In this example, the loop itself doesn’t produce a value; the function returns early when a condition is met. The pattern is clean, but it’s a function return, not a loop return.

while Versus loop with an if Check

Every while loop can be rewritten as a loop that contains an if !condition { break; } block, but the two constructs carry different intent:

  • while says “repeat as long as this is true”—the condition is front and center.
  • loop says “run forever, and I’ll decide inside when to stop”—useful when the exit logic is tangled with the rest of the body or when you need a return value.

Choose whichever makes the flow easiest to read. A common guideline: if the stop condition can be stated cleanly up front, use while; if the condition depends on mid‑iteration computation or you need break with a value, use loop.

Forgetting to update the condition variable can freeze your program:

A while loop that never changes the variables involved in the condition will run forever. In while countdown > 0, if you accidentally omit countdown -= 1;, the loop never terminates. This is one of the most frequent beginner bugs.

Iterating with for

The for loop is the workhorse of Rust iteration, consuming an iterator and executing a block once for each item. It’s the closest equivalent to C‑style for loops, but it operates on iterators instead of raw index arithmetic.

fn main() {
    for i in 0..5 {
        println!("i = {}", i); // prints 0, 1, 2, 3, 4
    }
}

The expression after in can be anything that implements IntoIterator—ranges, arrays, slices, vectors, HashMap keys, file lines, and user‑defined types.

How for Works Under the Hood

When you write for item in collection, Rust calls .into_iter() on collection and then repeatedly calls .next() on the resulting iterator. The loop ends when .next() returns None. This means the collection is consumed (ownership moves into the loop) unless you explicitly borrow it.

fn main() {
    let vec = vec!["a", "b", "c"];
    // for item in vec         — would take ownership of vec (consumes it)
    // for item in &vec        — borrows, yielding &String
    // for item in &mut vec    — mutable borrow, yielding &mut String
    for item in &vec {
        println!("{}", item);
    }
    // vec is still usable here because we borrowed it
}

For beginners, the mental model is simple: for lets you look at each element of a collection without worrying about indices. The compiler handles borrowing and ownership, preventing you from accidentally modifying a collection while iterating over it.

Pattern Matching in for Loops

The left side of a for loop is a pattern, so you can destructure tuples or enums directly:

fn main() {
    let pairs = [(1, "one"), (2, "two")];
    for (num, word) in pairs {
        println!("{} spelled is {}", num, word);
    }
}

for Always Returns Unit

Like while, for is a statement. It evaluates to () and cannot yield a value with break value. If you need to collect a result from an iteration, use an iterator adapter such as map/filter/collect, or accumulate a value in a variable outside the loop.

Prefer iterators over manual indexing:

Rust iterators are a zero‑cost abstraction—they compile down to the same machine code as an indexed loop, but they eliminate off‑by‑one errors and make borrowing rules explicit. Write for item in &collection unless you specifically need index arithmetic.

Loop Labels for Nested Loops

When you have a loop inside another loop, break and continue affect only the innermost loop by default. To break out of an outer loop, prefix it with a label.

fn main() {
    'outer: for x in 0..5 {
        for y in 0..5 {
            if x + y > 6 {
                println!("Breaking outer loop when x={}, y={}", x, y);
                break 'outer;
            }
        }
    }
}

Labels are 'identifier placed before the loop. They work with loop, while, and for. You can also use labels with continue to skip to the next iteration of an outer loop.

Labels are rarely needed, but they prevent convoluted workarounds with boolean flags or early returns when dealing with deeply nested loops.

break and continue in Depth

Both keywords give you fine‑grained control over loop execution.

break — Exit the Loop

break; immediately terminates the loop. In a loop, you can attach a value: break expression;. In while and for, only break; (no value) is allowed.

let guess: u32 = loop {
    // imagine reading input and parsing
    break 42; // loop evaluates to 42
};
println!("guess = {}", guess);

break can also carry a label to target an outer loop, as shown above.

continue — Skip the Rest of the Iteration

continue; jumps to the loop’s next iteration, skipping any code below it. In a for loop, that means advancing the iterator; in a while loop, the condition is re‑evaluated; in a loop, it simply restarts the block.

fn main() {
    for i in 1..=10 {
        if i % 2 == 0 {
            continue; // skip even numbers
        }
        println!("Odd: {}", i);
    }
}

continue also supports labels, so you can skip to the next iteration of an outer loop.

break value is exclusive to loop:

Code like let x = while true { break 42; }; or for i in 0..{ break 42; } will not compile. The compiler rejects break with a value outside of a loop expression. If you need a result from a condition‑controlled loop, use a loop with an if condition.

Why Rust Has loop

Other languages treat infinite loops as a pattern you build from while true or for(;;). Rust makes it a first‑class construct for two reasons: expressiveness and compiler certainty.

  1. Explicit intent — A loop block announces “this is meant to run until a break occurs.” A reader does not have to inspect a condition to understand that the loop is infinite by design.
  2. Value‑returning loops — The break value pattern is only possible in loop, giving Rust an elegant way to express “do this until you’re done, and hand back the result.” This replaces the common C idiom of a flag variable set just before a break.
  3. Compiler knowledge — The compiler knows a loop has no exit condition to evaluate, so it never issues unreachable‑related warnings when a loop appears in a tail position, and it can generate slightly better code for constructs that must run indefinitely.

A while true loop with a constant condition is often optimized identically, but loop communicates the intent more clearly and allows break with a value—making it the idiomatic choice for infinite loops in Rust.

Common Mistakes with Loops

Even experienced developers trip over these patterns. Recognizing them early saves debugging time.

Using while to Iterate Over a Collection by Index

// Avoid this when you can
let values = vec![10, 20, 30];
let mut i = 0;
while i < values.len() {
    println!("{}", values[i]);
    i += 1;
}

This works, but it’s verbose and prone to off‑by‑one errors. A for item in &values loop is shorter, safer, and equally fast.

Forgetting That for Consumes the Iterator

When you write for item in collection, the collection is consumed unless you borrow it. If you then try to use the collection afterward, you’ll get a borrow‑checker error (or a move error). The fix is to iterate over a reference: for item in &collection.

Expecting a while or for to Return a Value

New Rust developers sometimes write let result = while ... { break 10; }; and are surprised by a compile error. The rule is simple: only loop can carry a value. Use loop when you need that behavior.

Infinite Loops That Weren’t Supposed to Be Infinite

A loop without a break will run forever. While the compiler won’t warn you, your program will hang. Always verify that every code path inside a loop eventually hits a break, a return, or a panic (if that’s intentional).

Modifying a Collection While Iterating Over It

Rust’s borrow checker prevents you from mutating a vector while you hold an iterator to it:

let mut vec = vec![1, 2, 3];
for item in &vec {
    vec.push(*item); // error: cannot borrow `vec` as mutable because it is also borrowed as immutable
}

The idiomatic solution is to collect the modifications into a new collection, or to use an index‑based loop that doesn’t hold a borrow over the entire iteration.

Borrow checker errors often point to a loop body:

When you see a “cannot borrow x as mutable” error, check whether you are trying to mutate something that is already borrowed immutably by the loop itself. The fix is usually to restructure the loop or to use indexing.

Loops in Production Code

Loop constructs appear in every non‑trivial Rust codebase. Here are the typical patterns you’ll encounter:

  • Event loops and servers — A loop that calls .accept() or polls a future, often combined with match to handle different connection outcomes. This loop intentionally runs forever and only exits on a shutdown signal.
  • Parsers and state machines — A loop that reads bytes and breaks when a complete message is built. The break can carry the parsed value directly.
  • Game loopsloop is a natural fit for the core update/draw cycle of a game engine.
  • Iterating over filesfor loops over std::fs::read_dir() entries, lines of a file, or network streams are the everyday work of system scripts.
  • Accumulating results — A for loop that collects sums, finds maximums, or builds a report. Often this is eventually replaced with iterator combinators like fold or sum, but for is the starting point and remains perfectly readable.

Understanding how loop, while, and for fit into these real‑world contexts helps you see why Rust’s design distinguishes them so sharply. The language nudges you toward the clearest tool for the task: for for iteration, while for condition‑driven repetition, and loop for infinite patterns that produce a value.

Summary

Rust’s three loop forms each solve a distinct class of repetition problems, and their differences center on one question: can the loop produce a value? Only loop can; while and for always return (). This single fact determines when you reach for each construct.

  • loop — when you need to run until a condition is met inside the body, and you want to hand back a result. Think of it as a “try forever until you succeed, then give me the answer” block.
  • while — when the stop condition can be written cleanly before each iteration. It’s the direct translation of traditional conditional loops.
  • for — when you have an iterator. It’s the safest, most idiomatic way to walk through a collection, and it works seamlessly with Rust’s ownership system.

When loops nest, labels let you break or continue an outer loop without messy flags. And when you feel the urge to write a C‑style for(i=0; i<n; i++), remember that a for over a range or an iterator gives you the same performance with far fewer indexing hazards.