loop, while, and for

Almost every useful program repeats work: processing each item in a list, retrying an operation until it succeeds, or counting down to zero. Rust gives you three distinct tools for this — loop, while, and for — and each one exists for a different situation. Unlike most languages you may already know, Rust’s loop can actually produce a value when it finishes, and its for loop is built entirely on iterators rather than manual indexing, which is a large part of why Rust programs rarely crash with an out-of-bounds index.

Overview / How the three loops differ

loop is the simplest and most general form. It has no condition at all — it repeats its body forever until you explicitly stop it with break. Because the compiler knows a bare loop never ends on its own, it is the only loop form that is allowed to produce a value: you can write break value; instead of a plain break;, and that value becomes the result of the whole loop expression. This makes loop a good fit for "retry until it works" logic, where you don’t know in advance how many attempts it will take.

while repeats its body only as long as a boolean condition stays true. Rust checks the condition before every iteration, including the first one — if the condition is false immediately, the body never runs at all. Think of a countdown from 3: the loop checks "is number not zero?", runs the body and prints, decrements, checks again, and so on, until the check finally comes back false and control falls through to the line after the loop.

for is the workhorse for iterating over a collection or a range, and it is the loop you should reach for by default. It doesn’t use a numeric index at all under the hood — it repeatedly asks an iterator for its next item until the iterator reports there are no more. Because the iterator itself knows when it is exhausted, there is no off-by-one bookkeeping for you to get wrong, and no possibility of indexing past the end of a collection. Anything that implements the IntoIterator trait — arrays, slices, Vec<T>, ranges like 0..5, HashMap, and more — can be the target of a for loop.

All three loops support break (exit the loop immediately) and continue (skip the rest of the current iteration and go to the next one). When loops are nested, you can tag an outer loop with a 'label: and break or continue that specific loop from inside an inner one — otherwise break and continue only ever affect the innermost loop they appear in.

Syntax

loop {
    // repeats forever until `break`
}

while condition {
    // repeats while `condition` evaluates to true
}

for item in iterable {
    // repeats once for each item the iterator produces
}
Form Stops when Can return a value? Typical use
loop a break is reached yes, via break value; retry logic, event loops, unknown iteration count
while the condition becomes false no repeating while some state holds
for the iterator is exhausted no walking a collection or range

Ranges used with for

Syntax Meaning
a..b half-open range, includes a up to but not including b
a..=b inclusive range, includes both a and b
(a..b).rev() the same half-open range, walked backwards

Examples

Example 1: loop with a break value

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {}", result);
}
The result is 20

Each pass through the loop increments counter. Once it reaches 10, break counter * 2 both stops the loop and hands its value out as the result of the whole loop expression, which is then bound to result. Notice the semicolon after the closing brace of loop — the entire construct is being used as an expression assigned to a variable.

Example 2: while loop

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{}!", number);
        number -= 1;
    }

    println!("Liftoff!");
}
3!
2!
1!
Liftoff!

The condition number != 0 is checked before each iteration. As soon as number reaches 0 the condition is false, the loop body is skipped, and execution continues with the final println!.

Example 3: for loop over a collection

fn main() {
    let fruits = vec![String::from("apple"), String::from("banana"), String::from("cherry")];

    for (index, fruit) in fruits.iter().enumerate() {
        println!("{}: {}", index, fruit);
    }

    println!("We still own the vector: {:?}", fruits);
}
0: apple
1: banana
2: cherry
We still own the vector: ["apple", "banana", "cherry"]

fruits.iter() produces an iterator of &String references without taking ownership of fruits, and .enumerate() wraps each item with its position, yielding (usize, &String) pairs. Because we only borrowed the vector, fruits is still usable after the loop, as the last line proves.

Example 4: labeled loops

fn main() {
    let mut count = 0;

    'counting_up: loop {
        println!("count = {}", count);
        let mut remaining = 10;

        loop {
            println!("remaining = {}", remaining);
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }

    println!("End count = {}", count);
}
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2

There are two nested loops here, and a plain break inside the inner loop only ever stops the inner loop — that’s why remaining == 9 just breaks back out to the outer loop each time. The label 'counting_up lets break 'counting_up reach all the way out of both loops at once, which is what finally ends the program once count hits 2.

How it works step by step

while condition { body } is essentially sugar for a loop with a manual check: internally the compiler produces something equivalent to loop { if !condition { break; } body }. That’s why while can never yield a value — its underlying loop only ever uses a bare break.

for item in iterable { body } desugars further, into a loop driven by the Iterator trait. Roughly, the compiler turns it into calling iterable.into_iter() once to obtain an iterator, and then repeatedly calling .next() on it inside a loop: each call returns an Option<Item>, and a Some(item) runs the body with that value bound while a None triggers a break. This is exactly why a for loop can never index out of bounds: the iterator itself is responsible for knowing when it has nothing left to give, instead of you comparing an index against a length by hand.

This desugaring also explains why for word in words can take ownership of words while for word in &words does not: the first calls into_iter() on Vec<String> itself, which consumes the vector and yields owned String items, while the second calls into_iter() on &Vec<String>, which only borrows and yields &String references.

Common Mistakes

Mistake 1: using a collection by value inside a for loop, then using it again

Writing for word in words moves words into the loop (via into_iter()), so trying to use words afterward is a compile error, not a runtime issue:

fn main() {
    let words = vec![String::from("hello"), String::from("world")];

    for word in words {
        println!("{}", word);
    }

    println!("{:?}", words);
}
error[E0382]: borrow of moved value: `words`
  |
  | for word in words {
  |             ----- `words` moved due to this implicit call to `.into_iter()`
  |
  | println!("{:?}", words);
  |                   ^^^^^ value borrowed here after move

The fix is to iterate by reference with &words (or .iter()) so the loop only borrows each element instead of taking ownership:

fn main() {
    let words = vec![String::from("hello"), String::from("world")];

    for word in &words {
        println!("{}", word);
    }

    println!("{:?}", words);
}
hello
world
["hello", "world"]

Mistake 2: off-by-one index causing a runtime panic

This mistake compiles fine — Rust’s type system has no way to know your bound is wrong — but it panics as soon as it runs, because array indices only go from 0 up to (but not including) len():

fn main() {
    let scores = [10, 20, 30];

    let mut i = 0;
    while i <= scores.len() {
        println!("{}", scores[i]);
        i += 1;
    }
}
10
20
30
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 3'
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

scores.len() is 3, and valid indices are 0, 1, and 2 — but i <= 3 lets i reach 3 before the loop stops, and scores[3] is out of bounds. The idiomatic fix is to avoid manual indexing altogether and let a for loop drive the iteration:

fn main() {
    let scores = [10, 20, 30];

    for score in scores.iter() {
        println!("{}", score);
    }
}
10
20
30

Best Practices

  • Default to for when walking a collection or range — it removes the possibility of an off-by-one indexing panic entirely.
  • Iterate by reference (&collection or .iter()) unless you specifically need to consume and own each element; iterate with .iter_mut() only when you need to modify elements in place.
  • Reach for loop with break value; when you need a "repeat until this succeeds, then give me the result" pattern, such as retrying an operation.
  • Use labeled break/continue instead of a boolean "found" flag to escape nested loops — it’s clearer and avoids an extra mutable variable.
  • Prefer the half-open range a..b for lengths and counts; reach for the inclusive a..=b only when the upper bound genuinely belongs in the sequence.
  • Avoid writing a manual index variable and a length comparison when an iterator adapter (.enumerate(), .rev(), .zip()) already expresses the same intent more safely.
  • Give an infinite loop a clear, obvious exit condition — a loop with no reachable break at all will never terminate.

Practice Exercises

  • Write a program that uses loop and break value; to sum the integers from 1 to 100, and prints the total. (Expected output: 5050.)
  • Write a program that uses a while loop to reverse the digits of the number 1234 using arithmetic (% 10 and /= 10), printing the reversed number. (Expected output: 4321.)
  • Given two arrays of integers, use a labeled for loop nested inside another for loop to find the first pair (one from each array) whose product equals a target value, then break out of both loops as soon as it’s found and print the pair.

Summary

  • loop repeats unconditionally and is the only loop that can produce a value, via break value;.
  • while repeats as long as a condition stays true, checked fresh before every iteration.
  • for iterates over anything implementing IntoIterator and is built on repeated calls to .next(), so it can never run past the end of a collection.
  • break and continue normally affect only the innermost loop; a 'label: lets you target an outer loop explicitly.
  • Iterating a collection by value moves it; iterate by reference (&collection or .iter()) to keep using it afterward.
  • Prefer for over a manually indexed while loop to eliminate off-by-one indexing panics.