Replace Control Flag with Break/Return
Intent
Section titled “Intent”A control flag is a variable whose only purpose is to decide whether a loop keeps running. You set it, you test it at the top of the loop, you flip it somewhere in the middle, and the loop notices on the next pass. Replace Control Flag with Break/Return removes the variable entirely and lets the loop’s exit be expressed directly with break, return, or continue at the exact moment the decision is made.
The control structures of a language already know how to stop and skip. A flag is a hand-rolled, delayed version of that — so the language construct, used directly, is both shorter and clearer.
The smell
Section titled “The smell”You see a boolean named found, done, or finished, declared just before a loop and read in the loop’s condition. To understand when the loop ends you have to scan the whole body hunting for every place the flag is flipped, then reason about when the condition will next be tested. The exit is smeared across several lines instead of stated where it happens. A flag that is also used after the loop is a stronger signal still: it is really two ideas — “stop looping” and “report a result” — sharing one variable.
Before → After
Section titled “Before → After”Searching a list for a customer who owes money. Before, a found flag drives a while. After, the function returns the moment it finds one.
// Beforefunction hasDebtor(customers: Customer[]): boolean { let found = false; let i = 0; while (!found && i < customers.length) { if (customers[i].balance < 0) { found = true; } i++; } return found;}
// Afterfunction hasDebtor(customers: Customer[]): boolean { for (const customer of customers) { if (customer.balance < 0) { return true; } } return false;}# Beforedef has_debtor(customers): found = False i = 0 while not found and i < len(customers): if customers[i].balance < 0: found = True i += 1 return found
# Afterdef has_debtor(customers): for customer in customers: if customer.balance < 0: return True return False// Beforefunc HasDebtor(customers []Customer) bool { found := false i := 0 for !found && i < len(customers) { if customers[i].Balance < 0 { found = true } i++ } return found}
// Afterfunc HasDebtor(customers []Customer) bool { for _, c := range customers { if c.Balance < 0 { return true } } return false}// Beforefn has_debtor(customers: &[Customer]) -> bool { let mut found = false; let mut i = 0; while !found && i < customers.len() { if customers[i].balance < 0.0 { found = true; } i += 1; } found}
// Afterfn has_debtor(customers: &[Customer]) -> bool { customers.iter().any(|c| c.balance < 0.0)}flowchart LR
subgraph Before["Before"]
A["found = false"] --> B["loop while !found"]
B --> C["set found = true"]
C --> B
end
subgraph After["After"]
D["loop"] --> E["break / return"]
end
Before -.->|"Replace Control Flag"| After Mechanics
Section titled “Mechanics”- Find the loop and the flag that controls it. Confirm the flag exists only to govern the loop — that it is not also carrying a result the code needs afterward.
- Decide on the replacement. If the loop does no more work after the flag is set,
returnthe answer right there. If there is work after the loop, usebreakto leave it. To skip the rest of one iteration only, usecontinue. - Replace the line that sets the flag with the chosen jump.
- Remove the flag from the loop condition, simplifying the condition to whatever genuinely remains (often the iteration bound, or nothing — a plain
for…of). - Delete the flag declaration and any final
return flag, replacing it with the value the loop now falls through to. - Run your tests after each removal so a regression points at the one line you just changed.
When to use / trade-offs
Section titled “When to use / trade-offs”Use this whenever a boolean exists purely to end or skip a loop. The payoff is that the exit condition appears exactly where the decision is taken, so a reader sees why the loop stops without simulating it in their head. It also tends to unlock further cleanup: once the flag is gone, the loop body is often a candidate for a built-in like any, find, or filter.
The caution is overusing jumps. A handful of break/return exits read clearly; a loop with five continues and three breaks buried in nested conditions can be harder to follow than the flag was. If removing the flag would scatter many exits through deep nesting, first apply Replace Nested Conditional with Guard Clauses to flatten the body, or extract the loop into its own function so each early exit is a clean answer. The inverse is rarely worth doing: reintroducing a flag almost never improves readability.
Related
Section titled “Related”- Replace Nested Conditional with Guard Clauses
- Decompose Conditional
- Consolidate Conditional Expression