Right, let’s talk about loops that don’t know when to quit. The while loop is the workhorse of conditional repetition. It’s the “just keep swimming” of Rust, executing a block of code as long as its condition holds true. It looks exactly like you’d expect from any C-style language:

let mut counter = 0;

while counter < 5 {
    println!("Counter is at: {}", counter);
    counter += 1;
}

println!("Done! Counter reached {}", counter);

Simple. Clean. It will print {{< bibleref “Numbers 0 ” >}} through 4 and then bail out. The beauty and the terror of the while loop lie in its condition. Get that condition wrong, and you’ve just invented a new way to heat your CPU. An infinite loop isn’t inherently evil—sometimes you want a server to run until the heat death of the universe—but accidentally creating one is a rite of passage. If your fans suddenly sound like a jet engine, check your while condition first.

The loop Keyword: For When You’re Absolutely Certain

Before we get fancier, I have to mention Rust’s loop keyword. It’s an infinite loop on purpose, with a very clear escape hatch: break.

let mut counter = 0;

loop {
    println!("Counter is at: {}", counter);
    counter += 1;

    if counter >= 5 {
        break;
    }
}

You might be thinking, “Why not just use while true?” You can, but loop is semantically clearer. It tells everyone, including the compiler, “I fully intend to loop forever unless I explicitly break out.” This clarity can sometimes help the compiler make better optimizations. It’s the difference between falling asleep and being knocked out; the result is similar, but the intent is different.

while let: Pattern Matching’s Loop Assistant

This is where Rust gets clever. You remember how if let gives us a concise way to handle a single Option or Result? while let is its loop-based cousin. It’s a fantastic tool for peeling elements out of an iterator or consuming values from a stream until the pattern no longer matches.

Imagine you have a vector and you want to pop items off the end until it’s empty. You could do it the tedious way:

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

while !stack.is_empty() {
    let top = stack.pop().unwrap();
    println!("Popped: {}", top);
}

This works, but it’s clunky. We’re manually checking for emptiness and then using .unwrap() because we know .pop() will return Some… but the compiler doesn’t. It’s inelegant.

Enter while let. This construct beautifully combines the condition check and the value extraction:

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

while let Some(top) = stack.pop() {
    println!("Popped: {}", top);
}

Read this as “while stack.pop() returns Some(top), bind the inner value to top and run this block.” The moment pop() returns None, the loop gracefully terminates. It’s more concise, safer (no .unwrap() in sight), and clearly communicates the intent: “keep going as long as there’s something there.”

The Iterator Alternative (And Why It’s Usually Better)

Here’s the part where I be honest with you. While while let is perfect for the example above, most of the time you’re iterating over a collection, you’re better off using a proper for loop. The for loop is more idiomatic and often clearer.

Our while let example is actually a bit forced. If you just have a Vec, you’d do this:

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

for item in stack.iter().rev() {
    println!("Item: {}", item);
}
// Or, to actually consume it:
for item in stack.into_iter().rev() {
    println!("Item: {}", item);
}

So, when should you use while let? Its superpower is handling ad-hoc iterators or streams where the number of items isn’t known upfront. The classic example is receiving values from a channel:

// Assuming a channel receiver `rx`
while let Ok(message) = rx.try_recv() {
    println!("Received: {}", message);
}

This loop will tirelessly try to receive messages and process them until the channel’s sender side is dropped and try_recv() starts returning Err(...), breaking the pattern. This is its sweet spot: looping until a producer is finished.

A Word of Caution: Scope and Ownership

Remember, while let follows the same ruthless ownership rules as everything else in Rust. The pattern binding (top in our earlier example) is owned by the block for that single iteration. This is usually what you want, but be mindful if you’re trying to move values out or store references across iterations.

The while loop is a fundamental tool. Use it for simple conditional loops. Embrace while let for concisely consuming conditional sequences. But always ask yourself: “Could a for loop or a method like .for_each() do this more clearly?” The answer will often be yes, and your code will be better for it.