RFA-570 · Case file with fixtures · Case 542 of 694 · Compiler evidence
Rust break in a while Condition Needs an Explicit Loop Target
A while condition is evaluated before its body becomes the implicit destination of unlabeled loop control. Name the destination or express the search as a loop.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- The condition decides whether the loop body begins, so that special position cannot use the loop as an implicit unlabeled control-flow destination.
- First discriminating check
- Name the intended destination explicitly or restructure the state machine as an ordinary loop whose body contains its exits.
An unlabeled break normally exits the nearest enclosing loop. That simple rule has one boundary which is easy to miss: the condition of a while loop is not allowed to target that loop implicitly. Rust reports E0590 instead of guessing which control-flow operation I intended.
The failing fixture increments an attempt counter inside a condition block and then writes break. It looks enclosed by while on the page, but the implicit destination is not available there.
A while condition decides whether the body starts
The useful mental model is to expand while condition { body } into repeated condition evaluation followed by a conditional body. The condition runs before each iteration is admitted. A bare break or continue in that expression would rely on the very implicit loop destination whose next iteration is still being decided.
The official E0590 explanation permits control flow there only when an explicit label removes the ambiguity. This is a language rule, not a borrow-checker limitation and not an optimiser problem.
I first ask why the condition needs to perform loop control at all. A condition is easiest to review when it computes a boolean. If it is also incrementing counters, reading input, choosing a result, and escaping, an ordinary loop usually makes the state machine clearer.
The repair makes the search operation visible
The repaired fixture uses a labelled loop. The counter update and exit now live in the loop body, and break 'search names the operation being completed. The assertion records the expected number of attempts.
This repair is deliberately small, but the idea scales. In a parser I may have an outer record loop and an inner byte scan. In a retry controller I may have a request loop around a response-classification loop. Labels such as 'records, 'scan, or 'retry communicate the destination better than 'outer and 'inner, which become wrong after refactoring.
Rust's loop expression documentation also matters because loop can return a value through break expression. Instead of mutating an Option declared outside, I can write let found = 'search: loop { ... break 'search value; };. This keeps the result tied to the operation that produces it.
A label is not a jump to arbitrary code
Rust labels apply to loops and labelled block expressions. They are lexical destinations, not general goto labels. The target must enclose the break, and continue targets loops rather than labelled blocks. Those restrictions keep local control flow structured.
When I see E0590, adding a label mechanically can compile while leaving difficult code behind. I check whether the condition contains a hidden state machine. If it does, I extract that decision into a named function returning a boolean or result, or I promote the whole construct to a clear loop with explicit exits.
For example, a networking retry condition might call the clock, increment an attempt, inspect cancellation, and decide whether an error is retryable. I prefer a loop body that evaluates each reason in order. Reviewers can then see which exit wins and which metrics are updated.
Side effects in conditions deserve tests
A pure condition is simple to test at boundary values. A stateful condition needs tests for how many times it runs, including the initial evaluation and the final false evaluation. Off-by-one errors often hide there even after E0590 is fixed.
I test zero allowed attempts, one successful attempt, exhaustion, and cancellation. If the loop returns a value, I test each labelled exit. If nested loops exist, I use data that distinguishes exiting the inner scan from exiting the outer operation.
The compiler proves that the control-flow destination is legal. It cannot prove that I chose the business destination I wanted.
My E0590 checklist
- Is the
breakorcontinueinside awhilecondition rather than its body? - Which concrete operation should end or restart?
- Would an ordinary labelled
loopmake the state machine clearer? - Can the condition become a side-effect-free boolean?
- Does a returned value remove an outer mutable placeholder?
- Do label names describe roles rather than nesting positions?
- Are evaluation counts and boundary exits tested?
- After a refactor, does every label still target the intended operation?
The core principle is that control flow needs an unambiguous destination. Lexical indentation suggests a relationship, but Rust requires the special boundary inside a while condition to be explicit. I use that diagnostic as a prompt to make the whole operation, not only the label, easier to reason about.