7.4loop and while

Last updated June 12, 2026

Conditionals choose a path once. Loops walk a path repeatedly, and they're the reason a twelve-line program can do a billion things. Rust has three loop statements; this lesson covers while and loop, and the next covers for.

while

A while loop checks a condition, and if it's true, runs its block, then checks again. And again. The loop ends the first time the check comes up false:

fn main() {
    let mut count = 1;
    while count <= 10 {
        print!("{count} ");
        count += 1;
    }
    println!("done!");
}
1 2 3 4 5 6 7 8 9 10 done!

(print! is println! minus the newline, so the numbers land on one line.)

Trace it like the machine does. count is 1; 1 <= 10 is true; print, bump count to 2; check again; ... ; print 10, bump to 11; 11 <= 10 is false; the loop is over and execution continues below it. Each full pass through the block is called an iteration; this loop ran ten of them.

The condition is checked before every iteration, including the first. So a while loop whose condition starts out false runs zero times:

let mut count = 15;
while count <= 10 {   // false on arrival; body never runs
    count += 1;
}

The usual anatomy is the one in the first example: a counter initialized before the loop, a condition that tests it, and an update inside the block. Forget the update and the condition never changes, which means the loop never ends. This is called an infinite loop, and writing one by accident is a rite of passage (your program just... sits there; Ctrl-C in the terminal ends it). When it happens, check the update first.

Deliberate infinite loops: loop

Sometimes forever is the point. A game keeps taking turns until somebody wins; a server answers requests until it's shut down. Neither knows at the top of any iteration whether it's the last. The C-family idiom is while (true). Try the equivalent here and rustc has an opinion:

fn main() {
    while true {
        println!("again!");
    }
}
warning: denote infinite loops with `loop { ... }`
 --> src/main.rs:2:5
  |
2 |     while true {
  |     ^^^^^^^^^^ help: use `loop`
  |
  = note: `#[warn(while_true)]` on by default

Rust has a dedicated keyword for the intentional infinite loop:

loop {
    println!("again!");   // runs until something inside stops it
}

This isn't compiler pedantry about spelling. while true is a while loop whose condition happens to be constant; loop is a promise that there is no condition. The compiler takes the promise seriously: it knows a loop can only be left deliberately, which unlocks abilities while true doesn't get (lesson 7.6 cashes this in). For now, the rule is simple:

Best practice

Write intentional infinite loops with loop, never while true. It states the intent, and the compiler rewards the honesty.

Of course, "infinite" loops almost always end eventually; they just decide from the inside. The escape hatch is the break statement, which immediately ends the loop it's in. Here's the everyday shape, a menu that re-asks until the answer makes sense:

use std::io;

fn main() {
    loop {
        println!("Please pick an option (1-4):");
        let mut input = String::new();
        io::stdin().read_line(&mut input).expect("failed to read line");
        let choice: u32 = input.trim().parse().expect("please type a number");
        if choice >= 1 && choice <= 4 {
            println!("you picked option {choice}");
            break;
        }
        println!("{choice} is not on the menu, try again.");
    }
    println!("goodbye!");
}
Please pick an option (1-4):
7
7 is not on the menu, try again.
Please pick an option (1-4):
2
you picked option 2
goodbye!

The reading-and-parsing lines are lesson 5.6's idiom, working for a living. break gets its full lesson at 7.6; for now, "ends the loop, immediately" is the whole story.

One more thing this example settles. C++ has a third loop, do-while, whose condition sits at the bottom, for exactly this run-at-least-once situation (you can't test the choice before you've asked for it). Rust doesn't have do-while, and the example shows why nobody misses it: loop plus a conditional break is the same machine, with the exit condition placed wherever it actually belongs, top, bottom, or middle.

The unsigned countdown trap

Now a counting loop that goes down, with a bug, and the type system has opinions. Suppose we're counting down to liftoff and, mindful of lesson 4.2, we pick u32 since a countdown is never negative:

fn main() {
    let mut i: u32 = 3;
    while i >= 0 {
        println!("{i}");
        i -= 1;
    }
    println!("liftoff!");
}

The plan: print 3, 2, 1, 0, then i becomes -1, the condition fails, liftoff. The compiler spots the hole in that plan before running a single line:

warning: comparison is useless due to type limits
 --> src/main.rs:3:11
  |
3 |     while i >= 0 {
  |           ^^^^^^
  |
  = note: `#[warn(unused_comparisons)]` on by default

A u32 is always >= 0; the condition can never fail; this loop is infinite, and rustc said so politely. Run it anyway and the second shoe drops: after printing 0, the i -= 1 is the overflow from lesson 4.4, and the debug build panics with attempt to subtract with overflow rather than wrapping i around to 4,294,967,295 and counting down from there (which is what the same bug does, silently, in C++).

The fix is to stop before the subtraction goes wrong, and to make the loop's last useful iteration explicit:

fn main() {
    let mut i: u32 = 3;
    while i > 0 {
        println!("{i}");
        i -= 1;
    }
    println!("liftoff!");
}
3
2
1
liftoff!

(If you want the 0 printed before liftoff, print first and ask questions later: condition i > 0 with the print moved, or just use an i32. The point isn't the cosmetics; it's that while i >= 0 on an unsigned counter is always a bug, and here it's a labeled one.)

Loops in loops

A loop's block is just a block, so it can contain another loop. The inner loop runs to completion on every iteration of the outer one:

fn main() {
    let mut outer = 1;
    while outer <= 5 {
        let mut inner = 1;
        while inner <= outer {
            print!("{inner} ");
            inner += 1;
        }
        println!();
        outer += 1;
    }
}
1 
1 2 
1 2 3 
1 2 3 4 
1 2 3 4 5 

Note where inner is declared: inside the outer loop, so it's recreated at 1 for every row. Declare it once above both loops and it would keep its value between rows, printing one ever-lengthening line. (This is lesson 1.11's scoping rules again, doing useful work: the tightest scope isn't just tidy, it's the correct behavior.)

Quiz time

Question #1

How many iterations does this loop run, and what does it print?

fn main() {
    let mut n = 1;
    while n < 100 {
        n *= 2;
    }
    println!("{n}");
}
Show solution
128

Seven iterations: n doubles through 2, 4, 8, 16, 32, 64, 128, and the check 128 < 100 fails. Note the printed value is the first power of two past the limit, not the last one under it; loops end one step after their condition stops being true, a classic source of off-by-one thinking.

Question #2

Write a program that prints the lowercase alphabet with character codes, one per line: a = 97 through z = 122. Use a while loop. (Hint: b'a' is a byte literal, the u8 code of 'a', and lesson 4.10's as can turn a code back into a char.)

Show solution
fn main() {
    let mut code = b'a';
    while code <= b'z' {
        println!("{} = {}", code as char, code);
        code += 1;
    }
}

The first lines of output:

a = 97
b = 98
c = 99

and so on, 26 lines in all, ending with z = 122. The counter is the character code; we cast it to char only for display. (Why stop at b'z' and not worry about overflow? 122 is comfortably inside u8's range, and the loop ends there. Past b'z', the += 1 would be fine right up until 255.)

Question #3

Modify the nested-loop example to print the triangle upside down: first row 1 2 3 4 5, last row 1.

Show solution
fn main() {
    let mut outer = 5;
    while outer >= 1 {
        let mut inner = 1;
        while inner <= outer {
            print!("{inner} ");
            inner += 1;
        }
        println!();
        outer -= 1;
    }
}
1 2 3 4 5 
1 2 3 4 
1 2 3 
1 2 
1 

The outer counter runs 5 down to 1 (safely: it's an i32 by inference, and the condition >= 1 stops before anything goes negative even for unsigned types). The inner loop is unchanged; the rows shrink because their limit shrinks.

Next: the loop that counts for you. for, ranges, and the end of six irritating lines from chapter 4.