Control Flow
Rule
Use explicit clarity-first branches and keep mutation in small scopes after fallible validation.
Why
Visible exits, exhaustive state handling, and atomic-looking updates make invariants easier to preserve during later changes.
Do
- Use
?when the current function only propagates failure. - Use early returns for invalid inputs, missing prerequisites, and permission failures.
- Use
let elsewhen a missing pattern exits the current scope. - Use
if letfor one special pattern andwhile letfor repeated extraction. - Use
matchwhen several variants or exhaustiveness matter. - Keep arms and guards small and name meaningful variants instead of
_. - Keep the happy path linear after validation.
- Use small mutable locals for accumulators, builders, and staged values.
- Compute and validate before mutating long-lived state when partial updates would be invalid.
Avoid
- Do not match on
bool. - Do not hide meaningful states behind
_. - Do not deeply nest branches when guard clauses clarify exits.
- Do not use
let elsefor substantial recovery logic. - Do not replace error handling with
unwraporexpect. - Do not mutate persistent state before fallible validation unless partial progress is intentional.
- Follow Option and Result for combinator choices.
Example
#![allow(unused)]
fn main() {
pub fn plan_action(request: Request) -> Result<Action, Error> {
let Some(user_id) = request.user_id() else {
return Err(Error::MissingUserId);
};
let command = Command::parse(request.command())?;
if !request.permissions().can_run(user_id, &command) {
return Err(Error::Forbidden { user_id });
}
match command {
Command::Start(target) => Ok(Action::Start(Target::try_new(target)?)),
Command::Stop(target) => Ok(Action::Stop(target)),
Command::Status => Ok(Action::Status),
}
}
}
Exceptions
- Use
_for intentionally ignored test or metrics cases and external non-exhaustive enums. - Mutate incrementally when every step is independently valid and no rollback exists.