Loop Labels and Breaking with Values

In Rust, a loop label is a name you attach to a loop so that break and continue can target that exact loop, even from several levels deep inside nested loops. Loops are also expressions in Rust: a plain loop can hand back a value directly through its break statement, something most C-family languages have no equivalent for. Together, labels and break-with-a-value let you write nested search-and-exit logic that reads cleanly, without boolean “found” flags or awkward early returns from helper functions.

Overview / How it works

Rust has three looping constructs: loop, while, and for. All three can be used as statements, but only loop can be used as an expression that produces a value. The reason is about guarantees: a while loop or a for loop might not run its body even once — the condition could be false immediately, or the iterator could be empty — so the compiler has no value to offer if you try to use the loop’s result. A plain loop, on the other hand, never ends on its own; the only way out is a break, so whatever expression follows break is guaranteed to be the loop’s result. That is why break value; is legal inside a bare loop but is a compile-time error inside while and for.

Labels solve a different problem: which loop does break or continue actually affect? By default, an unlabeled break or continue always applies to the innermost loop that encloses it — the nearest enclosing pair of curly braces that is a loop. If you nest loops to search a grid, an unlabeled break inside the inner loop only stops the inner loop; the outer loop keeps right on going. A label lets you name a loop — written as an identifier prefixed with a single quote, like 'search — and place that name right before the loop keyword. Then break 'search or continue 'search reaches straight past any loops nested in between and acts on the labeled one specifically. Think of nested loops as boxes inside boxes: an unlabeled break only ever opens the box you are standing in, but a label is like writing a name on one specific box so you can jump straight to it regardless of how many boxes are in between.

One more rule matters once you combine labels with break-values: every break 'label value; that targets a given labeled loop must produce a value of the same type, because as an expression that loop has exactly one static type. The compiler checks this the same way it checks the arms of a match expression.

Syntax

'label: loop {
    // loop body
    if some_condition {
        break 'label value; // exits the labeled loop, optionally with a value
    }
    if other_condition {
        continue 'label; // jumps to the next iteration of the labeled loop
    }
}
Form Meaning
'label: loop { ... } Attaches 'label to this loop; the label’s scope covers the loop and everything nested inside it.
break; Exits the nearest enclosing loop. That loop evaluates to ().
break value; Exits the nearest enclosing loop and makes it evaluate to value. Only legal when that loop is a plain loop.
break 'label; Exits the loop tagged 'label specifically, unwinding out of any loops nested inside it.
break 'label value; Exits the loop tagged 'label and makes it evaluate to value. Only legal when that labeled loop is a plain loop.
continue; Skips the rest of the current iteration of the nearest enclosing loop and starts its next iteration.
continue 'label; Skips to the next iteration of the loop tagged 'label, skipping past any loops nested inside it.

Examples

Example 1: a plain loop returning a value

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {}", result);
}

Output:

The result is 20

The loop keeps incrementing counter on every pass. Once counter reaches 10, break counter * 2; both stops the loop and supplies its value. That value becomes the result of the whole loop { ... } expression, which is why it can be assigned straight into result with a let binding, no separate mutable variable needed to smuggle the answer out.

Example 2: labeling a loop to break the outer one

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {}", count);
        let mut remaining = 10;

        loop {
            println!("remaining = {}", remaining);
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {}", count);
}

Output:

count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2

The outer loop is labeled 'counting_up. The inner loop’s plain break; only ever stops the inner loop once remaining hits 9, letting the outer loop continue and increment count. But when count reaches 2, the inner loop instead runs break 'counting_up;, which exits the outer loop directly — the program jumps straight to the final println! without finishing that inner iteration or returning to the top of the outer loop.

Example 3: a labeled break carrying a value out of nested loops

fn main() {
    let grid = vec![
        vec![1, 3, 5],
        vec![7, 9, 11],
        vec![13, 15, 17],
    ];
    let target = 9;

    let location = 'search: loop {
        for (row_index, row) in grid.iter().enumerate() {
            for (col_index, &value) in row.iter().enumerate() {
                if value == target {
                    break 'search Some((row_index, col_index));
                }
            }
        }
        break 'search None;
    };

    match location {
        Some((row, col)) => println!("Found {} at row {}, col {}", target, row, col),
        None => println!("{} was not found in the grid", target),
    }
}

Output:

Found 9 at row 1, col 1

Here the outermost construct is a plain loop labeled 'search, wrapping two for loops that scan the grid. As soon as a matching cell is found, break 'search Some((row_index, col_index)); jumps out of both for loops at once and supplies the outer loop’s value. If the scan finishes without a match, the unconditional break 'search None; after the nested loops supplies the other possible value. Both break sites produce an Option<(usize, usize)>, so the compiler accepts them as two arms of the same type feeding the same labeled loop.

How it works step by step

Walking through Example 3: the compiler sees a loop labeled 'search used in a let binding, so it treats that loop as an expression and looks at every break 'search ... reachable from inside it to infer its type. It finds two: Some((row_index, col_index)) and None, both of type Option<(usize, usize)>, so location gets that type. At runtime, execution enters the 'search loop, then the outer for loop over rows, then the inner for loop over columns. The moment value == target is true, break 'search does not just stop the inner for loop — it unwinds out of the inner for, out of the outer for, and out of the 'search loop in one step, dropping any loop-local state (like the iterators) along the way. If no match is found, both for loops run to completion normally, and control falls through to the unconditional break 'search None; line.

continue works the same way but resumes the labeled loop’s next iteration instead of leaving it entirely:

fn main() {
    let mut found_pairs = 0;

    'outer: for x in 1..=3 {
        for y in 1..=3 {
            if x == y {
                continue 'outer;
            }
            found_pairs += 1;
            println!("Pair: ({}, {})", x, y);
        }
    }

    println!("Total pairs: {}", found_pairs);
}

Output:

Pair: (2, 1)
Pair: (3, 1)
Pair: (3, 2)
Total pairs: 3

Whenever x == y, continue 'outer; abandons the rest of the inner for loop for that x and jumps straight to the next value of x in the outer loop, skipping any remaining y values. Without the label, an unlabeled continue would only skip to the next y, not the next x.

Common Mistakes

Mistake 1: trying to break a value out of while or for

Only a plain loop is guaranteed to run until a break stops it, so only loop can evaluate to a value. Trying the same trick with while fails to compile:

fn main() {
    let mut i = 0;

    let result = while i < 5 {
        i += 1;
        if i == 3 {
            break i;
        }
    };

    println!("{}", result);
}

// error[E0571]: `break` with value from a `while` loop
// note: `while` loops evaluate to `()` and cannot produce a value

The fix is to switch to a bare loop and add the exit condition inside the body yourself:

fn main() {
    let mut i = 0;

    let result = loop {
        i += 1;
        if i == 3 {
            break i;
        }
    };

    println!("{}", result);
}

Output:

3

Mistake 2: forgetting the label and only breaking the inner loop

This one is not a compile error — it is a logic bug, because an unlabeled break is perfectly legal, it just targets the wrong loop:

fn main() {
    let mut attempts = 0;

    for outer in 0..3 {
        for inner in 0..3 {
            attempts += 1;
            if outer == 1 && inner == 1 {
                println!("Found it, stopping...");
                break;
            }
        }
    }

    println!("Total attempts: {}", attempts);
}

Output:

Found it, stopping...
Total attempts: 8

The author likely expected the search to stop the moment a match is found, but break; only exits the inner for loop; the outer loop keeps iterating and the count keeps climbing to 8. Adding a label on the outer loop and targeting it explicitly fixes the bug:

fn main() {
    let mut attempts = 0;

    'search: for outer in 0..3 {
        for inner in 0..3 {
            attempts += 1;
            if outer == 1 && inner == 1 {
                println!("Found it, stopping...");
                break 'search;
            }
        }
    }

    println!("Total attempts: {}", attempts);
}

Output:

Found it, stopping...
Total attempts: 5

Best Practices

  • Reach for break value; inside a plain loop when a loop’s whole purpose is to search for and produce a single result — it avoids a mutable “result” variable declared before the loop and reassigned inside it.
  • Only add a label when you actually need to reach past an inner loop; if there is just one loop, or an unlabeled break/continue already does what you want, skip the label to keep the code simple.
  • Give labels descriptive names like 'search or 'outer rather than single letters — they read like a small comment explaining what the loop is for.
  • Keep every break 'label value; targeting the same loop returning the same type; if you find yourself wanting different shapes of data, wrap them in an enum or use Option/Result.
  • Prefer returning early from a small helper function over deeply labeled nested loops when the logic gets complex — labels are great for two or three levels, but heavily nested labeled loops hurt readability just like deeply nested labeled loops do in any language.
  • Remember labels are a separate namespace from lifetimes even though both start with a single quote — the compiler tells them apart from context, but a reader benefits from you not reusing a name that also appears as a lifetime nearby.

Practice Exercises

  • Write a program that uses a labeled loop to find the first number greater than 1000 that is divisible by both 7 and 11, incrementing a counter each pass and using break 'label value; to return it. Expected output: Found: 1001.
  • Given a Vec<Vec<char>> representing a small grid of letters, write nested for loops inside a labeled outer loop that search for the character 'z' and print its row and column as soon as it is found, using break 'label with no value once printed.
  • Write a program with two nested for loops from 1 to 5 that prints every pair (x, y) where x + y == 6, but uses continue 'outer to skip straight to the next x as soon as one such pair is printed for that x, instead of checking the remaining y values.

Summary

  • Only a plain loop can evaluate to a value via break value;, because it is the only loop guaranteed to run until something breaks it; while and for always evaluate to ().
  • A label is written as 'name: immediately before loop, while, or for, and gives that specific loop a name that break and continue can target directly.
  • An unlabeled break or continue always affects the innermost enclosing loop; add a label whenever you need to reach an outer loop from inside nested loops.
  • break 'label value; combines both features: it exits a specific labeled loop and supplies its value, even when the break statement itself sits inside other loops nested in between.
  • Every break 'label value; reachable for a given labeled loop must agree on the value’s type, exactly like the arms of a match expression.
  • Forgetting a label on an outer loop is not always a compile error — it can silently produce a logic bug where only the inner loop stops, so double-check nested loop exits do what you intend.