Control Flow

Common Control-Flow Patterns

Idiomatic Rust combines expression-oriented branching (if, match, let ... else) and structured iteration (for, while let, loop) into recurring patterns that keep functions flat, eliminate uninitialized state, and make impossible states unrepresentable.

1. Guard Clauses and let ... else Early Exits

Instead of nesting validation checks inside deep if / else pyramids ("arrow code"), place guard clauses and let ... else statements at the top of a function or loop body to reject invalid inputs early:

fn compute_unit_price(line: &str) -> Result<u32, &'static str> {
    let trimmed = line.trim();
    if trimmed.is_empty() {
        return Err("empty input line");
    }

    let Some((total_str, qty_str)) = trimmed.split_once('/') else {
        return Err("missing '/' delimiter");
    };

    let Ok(total) = total_str.trim().parse::<u32>() else {
        return Err("invalid total amount");
    };

    let Ok(qty) = qty_str.trim().parse::<u32>() else {
        return Err("invalid quantity");
    };

    if qty == 0 {
        return Err("quantity cannot be zero");
    }

    Ok(total / qty)
}

fn main() {
    for sample in ["120 / 4", "", "100 / 0", "bad_format"] {
        println!("{sample:<12} -> {:?}", compute_unit_price(sample));
    }
}
120 / 4      -> Ok(30)
             -> Err("empty input line")
100 / 0      -> Err("quantity cannot be zero")
bad_format   -> Err("missing '/' delimiter")

2. Immutable Initialization via Branching Expressions

Instead of declaring a mutable variable (let mut category = ...;) and overwriting it inside branches, initialize an immutable variable directly from the value of an if / else or match expression:

fn main() {
    let latency_ms = 42;

    // `tier` is initialized once and stays strictly immutable:
    let tier = match latency_ms {
        0..=20 => "Fast",
        21..=100 => "Normal",
        101..=500 => "Slow",
        _ => "Timeout Risk",
    };

    println!("{latency_ms} ms -> {tier}");
}
42 ms -> Normal

3. Bounded Retry Loop with loop and break value

When an operation may take several attempts before succeeding—up to a maximum retry limit—a loop expression returning Result or Option via break expresses both the success exit and exhaustion exit cleanly:

fn simulate_attempt(attempt: u32) -> Result<&'static str, &'static str> {
    if attempt < 3 {
        Err("temporary network timeout")
    } else {
        Ok("connected on attempt 3")
    }
}

fn connect_with_retries(max_attempts: u32) -> Result<&'static str, &'static str> {
    let mut attempt = 1;

    loop {
        match simulate_attempt(attempt) {
            Ok(msg) => break Ok(msg),
            Err(err) if attempt >= max_attempts => break Err(err),
            Err(err) => {
                println!("  Attempt {attempt} failed ({err}); retrying...");
                attempt += 1;
            }
        }
    }
}

fn main() {
    println!("Outcome: {:?}", connect_with_retries(5));
}
  Attempt 1 failed (temporary network timeout); retrying...
  Attempt 2 failed (temporary network timeout); retrying...
Outcome: Ok("connected on attempt 3")

4. Explicit State Machine Loop (enum + loop + match)

Complex protocols, lexers, and workflow engines are modeled in Rust by combining an enum representing the current state with a loop and an exhaustive match on (state, input):

  [ Idle ] ──(Start)──> [ Processing { progress } ] ──(Complete)──> [ Done ]
                               │             ^
                               └──(Advance)──┘
#[derive(Debug)]
enum JobState {
    Idle,
    Running { progress: u8 },
    Completed { total_steps: u8 },
}

fn run_state_machine() -> u8 {
    let mut state = JobState::Idle;

    loop {
        state = match state {
            JobState::Idle => {
                println!("State: Idle -> starting job");
                JobState::Running { progress: 1 }
            }
            JobState::Running { progress } if progress < 3 => {
                println!("State: Running (step {progress}/3)");
                JobState::Running {
                    progress: progress + 1,
                }
            }
            JobState::Running { progress } => {
                println!("State: Running (step {progress}/3) -> finishing");
                JobState::Completed {
                    total_steps: progress,
                }
            }
            JobState::Completed { total_steps } => {
                println!("State: Completed in {total_steps} steps");
                break total_steps;
            }
        };
    }
}

fn main() {
    let steps = run_state_machine();
    println!("Returned step count: {steps}");
}
State: Idle -> starting job
State: Running (step 1/3)
State: Running (step 2/3)
State: Running (step 3/3) -> finishing
State: Completed in 3 steps
Returned step count: 3

5. Two-Pointer Converging while Loop

When scanning a sorted slice or checking palindromes from both ends toward the middle, a while left < right loop updates two indices cleanly without risk of out-of-bounds access:

fn is_palindrome(bytes: &[u8]) -> bool {
    if bytes.len() <= 1 {
        return true;
    }

    let mut left = 0;
    let mut right = bytes.len() - 1;

    while left < right {
        if bytes[left] != bytes[right] {
            return false;
        }
        left += 1;
        right -= 1;
    }

    true
}

fn main() {
    println!("'radar' -> {}", is_palindrome(b"radar"));
    println!("'rust'  -> {}", is_palindrome(b"rust"));
}
'radar' -> true
'rust'  -> false

Choosing the Right Control-Flow Tool

Goal Idiomatic Rust Pattern
Transform or filter every element in a slice for x in slice or iterator chain (.iter().filter(...).map(...))
Unwrap Option / Result or return early let Some(x) = opt else { return ...; }; (or ? operator)
Map an enum or integer to a computed value let out = match input { ... };
Drain a queue, stack, or peekable iterator while let Some(item) = queue.pop() { ... }
Poll/retry until a condition produces a value let val = loop { ... break result; };