How to Use while let with Enums in Rust

Use while let to loop over enum variants by matching a specific pattern in the condition.

The loop that stops when the shape changes

You are building a game engine. Every frame, the input system produces a batch of events. Most frames contain movement commands. Occasionally, a player presses the quit button or triggers a settings change. You need to process every movement command in the current batch, but the moment you encounter a Quit event, the batch processing must stop. You do not want to write a giant match block inside a while loop just to handle one case. You want the loop itself to care about the shape of the data.

A standard while loop requires a boolean condition. You would need a flag variable to track whether you should continue. A for loop processes every item, which forces you to add a break statement inside the body. Rust offers a cleaner tool. while let combines a loop condition with pattern matching. The loop runs as long as the expression matches a specific pattern. When the pattern fails, the loop ends.

Pattern matching as a loop condition

while let is a control flow construct that merges looping and destructuring. The syntax looks like a let binding, but it sits inside a while loop. The right side is an expression that produces a value. The left side is a pattern. On every iteration, Rust evaluates the expression and tries to match it against the pattern. If the match succeeds, the variables in the pattern get bound, and the loop body executes. If the match fails, the loop breaks immediately.

Think of a mail sorter on a factory line. The machine grabs the next envelope. If the envelope has a red label, the machine stamps it and moves to the next one. If the envelope has a blue label, the machine stops. The loop does not check a counter. It checks the label. while let works the same way. The pattern is the label. The expression is the envelope. The loop continues only while the labels match.

This construct shines with enums and Option. Enums represent values with different shapes. Option wraps a value or indicates absence. while let lets you loop over a sequence of values, extracting data only when the shape matches your needs.

Minimal example: draining a vector

The most common use case is consuming a collection until a boundary appears. Vectors provide a pop method that removes the last element and returns Option<T>. If the vector is empty, pop returns None. If the vector has items, it returns Some(item). You can use while let to pop items until the vector is empty or until you hit a specific variant.

enum Message {
    Move { x: i32, y: i32 },
    Quit,
}

fn main() {
    let mut messages = vec![
        Message::Move { x: 10, y: 20 },
        Message::Move { x: 50, y: 60 },
        Message::Quit,
    ];

    // pop() returns Option<Message>.
    // The pattern matches Some(Message::Move { x, y }).
    // If pop() returns None or Some(Quit), the pattern fails and the loop stops.
    while let Some(Message::Move { x, y }) = messages.pop() {
        println!("Moving to ({}, {})", x, y);
    }
}

The output prints the two move coordinates. The loop stops when pop returns Some(Message::Quit). The pattern Some(Message::Move { x, y }) does not match Some(Message::Quit). The loop breaks. The Quit message remains in the vector, or rather, it was popped and discarded by the failed match. If you need to handle the non-matching case, while let is not the right tool. You need a loop with a match.

When the pattern fails, the loop ends. That is the contract.

What the compiler actually does

Rust desugars while let into a loop with a match statement. The compiler rewrites your code to check the pattern explicitly. This helps you understand the control flow. The desugared version looks like this:

loop {
    let temp = messages.pop();
    match temp {
        Some(Message::Move { x, y }) => {
            println!("Moving to ({}, {})", x, y);
        }
        _ => break,
    }
}

The compiler creates a temporary variable for the expression. It matches the temporary against the pattern. If the pattern matches, the body runs. If the pattern does not match, the _ arm executes a break. This reveals two important details. First, the expression is evaluated every iteration. If the expression has side effects, those side effects happen on every loop. Second, the pattern must be non-exhaustive. If the pattern matches every possible value, the _ arm is unreachable, and the loop never breaks.

The compiler warns you if the pattern is exhaustive. It suggests using while true instead. This warning prevents accidental infinite loops where the condition never fails. Trust the warning. If the compiler says the pattern matches everything, you have an infinite loop.

Realistic use: parsing tokens

Parsers often consume a stream of tokens until a delimiter appears. You might read identifiers until you hit a number or an end-of-file marker. while let makes this logic concise. You can destructure the token inside the loop condition and accumulate results.

enum Token {
    Word(String),
    Number(i32),
    Eof,
}

fn parse_words(tokens: &mut Vec<Token>) -> Vec<String> {
    let mut result = Vec::new();
    // Consume words until we hit a Number, Eof, or the vector is empty.
    // The pattern matches only Some(Token::Word(word)).
    while let Some(Token::Word(word)) = tokens.pop() {
        result.push(word);
    }
    result
}

fn main() {
    let mut input = vec![
        Token::Word("hello".into()),
        Token::Word("world".into()),
        Token::Number(42),
        Token::Word("ignored".into()),
    ];

    let words = parse_words(&mut input);
    println!("Parsed: {:?}", words);
    // Output: Parsed: ["world", "hello"]
}

The function pops tokens from the end of the vector. It pushes words into the result. When pop returns Some(Token::Number(42)), the pattern fails. The loop breaks. The number remains unconsumed by the loop, though it was popped and dropped by the match. If you need to preserve the delimiter, you must handle the non-matching case explicitly. while let is for consumption, not for branching.

Use while let to turn pattern matching into loop control. If you need to handle multiple cases, reach for a loop with match.

Handling Results and I/O

while let works with Result just as well as Option. This is useful for reading from streams where errors can occur. You can loop until an error appears, processing successful values as they arrive.

use std::io::{self, BufRead};

fn read_lines_until_error(reader: &mut impl BufRead) -> Vec<String> {
    let mut lines = Vec::new();
    let mut buffer = String::new();

    // read_line returns Result<usize, Error>.
    // The pattern matches Ok(bytes_read).
    // If read_line returns Err, the loop stops.
    while let Ok(_bytes) = reader.read_line(&mut buffer) {
        if !buffer.is_empty() {
            lines.push(buffer.clone());
            buffer.clear();
        }
    }

    lines
}

The loop reads lines until an error occurs. If read_line returns Ok, the body executes. If it returns Err, the loop breaks. This pattern is common in network programming and file processing. You process data until the stream fails. The error handling is implicit in the loop termination. If you need to handle the error, you must use a different structure. while let swallows the non-matching case.

Convention aside: The community often uses while let Ok(v) = ... for I/O loops. It signals that the loop continues on success and stops on failure. If you see this pattern, recognize it as a standard idiom for stream processing.

Pitfalls: infinite loops and exhaustive patterns

The most common mistake is creating an infinite loop by evaluating an expression that never changes. If the expression always matches the pattern, the loop runs forever.

let vec = vec![1, 2, 3];

// BAD: Infinite loop.
// iter() creates a new iterator every iteration.
// next() always returns Some(&1) because the iterator is fresh.
while let Some(x) = vec.iter().next() {
    println!("{}", x);
}

The expression vec.iter().next() calls iter() on every iteration. iter() returns a new iterator starting at the beginning. next() returns the first element. The pattern always matches. The loop never advances. The fix is to store the iterator and mutate it.

let vec = vec![1, 2, 3];
let mut iter = vec.iter();

// GOOD: The iterator advances each iteration.
// next() returns None when the iterator is exhausted.
while let Some(x) = iter.next() {
    println!("{}", x);
}

Another pitfall is using an exhaustive pattern. If the pattern matches every possible value, the compiler warns you. This warning exists to catch logic errors. If you write while let x = value, the pattern x matches everything. The loop never breaks. The compiler suggests while true.

let value = 42;

// WARNING: Pattern is exhaustive.
// The compiler warns: "this pattern has a wildcard, consider `while true`".
while let x = value {
    println!("{}", x);
}

If you intend an infinite loop, use while true. If you intend to loop until a condition fails, ensure the pattern is non-exhaustive. The compiler will call you out if the pattern matches everything. Treat the warning as a bug.

Mutable bindings require care. while let binds variables as immutable by default. If you need to mutate the value inside the loop, you must use ref mut in the pattern or ensure the expression returns a mutable reference.

let mut vec = vec![1, 2, 3];

// BAD: x is immutable. Cannot modify *x.
// while let Some(x) = vec.iter_mut().next() {
//     *x += 1; // Error: cannot assign to `*x`, as `x` is a `&i32`
// }

// GOOD: ref mut binds a mutable reference.
let mut iter = vec.iter_mut();
while let Some(ref mut x) = iter.next() {
    *x += 1;
}

The pattern Some(ref mut x) binds x as &mut i32. You can modify the value through the reference. This allows mutation inside the loop while preserving the borrowing rules.

Do not fight the borrow checker here. Use ref mut when you need to mutate values extracted from a loop.

Decision: choosing the right loop

Rust provides several looping constructs. Each serves a different purpose. Choose based on your control flow needs.

Use while let when you need to loop until a specific pattern fails, especially with Option or Result chains. Use while let when you are draining a collection until a boundary condition appears. Use while let when you want to destructure values inside the loop condition and discard non-matching cases.

Use for when you want to iterate over every item in a collection without early termination based on pattern shape. Use for when the iterator handles the termination logic internally. Use for when you need clean, idiomatic iteration over ranges, slices, or collections.

Use match when you need to handle multiple distinct cases in a single block or when the logic for each variant is complex and divergent. Use match when you need to extract values from different variants and perform different actions for each. Use match when the pattern must be exhaustive.

Use loop with break when you need complex exit conditions that span multiple lines or depend on state changes inside the body. Use loop when you need to handle both matching and non-matching cases within the same iteration. Use loop when the termination logic cannot be expressed as a single pattern match.

Reach for while let to simplify loops that depend on data shape. It reduces boilerplate and makes the exit condition explicit.

Where to go next