Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-527 · Case file with fixtures · Case 499 of 694 · Compiler evidence

Rust Break Needs an Enclosing Loop or Breakable Block

Break transfers control to a lexical loop or labelled block target; it is not a synonym for function return. Use return for the function boundary and break only where a target exists.

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
Break transfers control to one lexical construct rather than acting as a synonym for returning from the function boundary.
First discriminating check
Name the intended control owner, use return when the function is finished, and remember only loop or labelled blocks—not for or while—carry break values.

I wrote break directly in a function body. Rust emitted E0268 because there was no enclosing loop or labelled breakable block to exit.

The failing fixture distinguishes two boundaries people often merge: break exits a local control-flow construct; return exits a function.

Break always needs a lexical target

Inside loop, while, and for, an unlabelled break targets the innermost loop. A label can select an outer enclosing loop. A labelled block can also be a break target under Rust's breakable-block rules.

Outside these constructs, there is no continuation point for break to jump to. Rust rejects the statement instead of guessing that the function should end.

The official E0268 page shows the basic rule.

The repaired fixture returns from the function

The repaired fixture iterates values and uses return Some(value) when the first item is found. If the slice is empty, control reaches None.

My first attempted repair used break Some(value) in a for loop. Rust correctly rejected it with E0571 because value-carrying break is available for loop and labelled breakable blocks, not for.

The final evidence preserves the intended function result.

Return and break carry values differently

return expression produces the function's result immediately. A plain loop can itself be an expression whose value is chosen with break expression.

for and while do not support a non-unit break value. I store a result, use find, return from the function, or rewrite as a loop only when that model fits.

The target construct determines where the value goes.

Labels resolve nested loops, not functions

In nested loops, 'outer: loop { ... break 'outer; } makes the destination explicit. Labels are especially valuable when two levels are close together.

They do not provide a name for the function return boundary, and they cannot jump across a closure or function call. Rust keeps control flow lexical and locally inspectable.

I avoid labels that span large bodies where a helper function would clarify the operation.

Breakable blocks can compute one local result

A labelled block such as 'result: { ... break 'result value; ... } can exit early with a value without creating a loop. This can replace nested conditional scaffolding in a contained calculation.

I use it sparingly and label it descriptively. A function-level return is clearer when the entire function is complete, while a block result is useful when later cleanup or processing must continue.

The loop-expression Reference documents both forms.

Macros can move break out of its context

A macro that expands to break assumes its invocation occurs inside an acceptable target. Calling it from another context produces E0268 at an expansion site.

I prefer macros that return expressions or control signals unless loop-only use is essential and documented. Tests invoke them in every supported context so the hidden control-flow requirement stays visible.

Choose the construct from the result shape

If I need the first matching element, Iterator::find returns Option. If I need to accumulate until a condition, try_fold can carry state and stop. If the algorithm mutates several locals and has complex exits, a normal labelled loop is often easiest to audit.

The repaired fixture uses return because finding the first value completes the whole function. A labelled block would be better if later work still had to run. I select the smallest control owner whose exit corresponds to the intended result, which avoids both E0268 and hidden cleanup skips.

Cleanup may change the choice

Rust drops locals during ordinary return and break according to scope rules, but explicit work after a block will not run after a function return. If flushing, metrics, or state restoration is required, I use RAII guards or keep the exit inside a block so subsequent code executes. I do not duplicate cleanup before every return branch.

My E0268 checklist

  • What exact loop or labelled block should receive the break?
  • Is that target lexically enclosing the statement?
  • Should this operation return from the function instead?
  • Am I trying to carry a value out of for or while?
  • Would find, position, or another adapter express the result?
  • Could a labelled block preserve later function work?
  • Did a macro expand to break outside a valid context?
  • Does the repaired evidence test empty and non-empty paths?

The core principle is that control transfers have specific owners. break belongs to an enclosing loop or block; return belongs to the function whose result is complete.