Control Flow

loop

The loop keyword creates an unconditional infinite loop that executes its block of code repeatedly until the program explicitly exits the loop (using break or return) or terminates (via panic!, std::process::exit, or an external signal).

Basic Syntax and Execution Flow

A loop expression consists of the loop keyword followed by a brace-enclosed block { ... }:

fn main() {
    let mut counter = 1;

    loop {
        println!("Iteration {counter}");

        if counter == 3 {
            break;
        }

        counter += 1;
    }

    println!("Exited loop after {counter} iterations.");
}
Iteration 1
Iteration 2
Iteration 3
Exited loop after 3 iterations.

Unlike while or for, loop has no header condition. At the end of each pass through the body, execution jumps directly back to the top of the block:

               +--------------------------+
               |                          |
               v                          |
     +-------------------+                |
     | Enter `loop` body |                |
     +---------+---------+                |
               |                          |
               v                          |
     +-------------------+      No        |
     |  `break` reached? +----------------+
     +---------+---------+
               | Yes
               v
     +-------------------+
     |  Continue after   |
     |   closing `}`     |
     +-------------------+

Controlling Iteration Inside loop

Because loop does not test a condition automatically, control flow inside the body relies on explicit statements:

Keyword Effect Inside loop
break Immediately terminates the enclosing loop and transfers control to the statement after }
break value Terminates the enclosing loop and makes the loop expression evaluate to value
continue Skips the remainder of the current iteration and jumps directly to the start of the next iteration
return / return value Exits the entire enclosing function immediately, regardless of how deeply nested the loop is
fn main() {
    let mut n = 0;

    loop {
        n += 1;

        if n % 2 == 0 {
            continue; // Skip even numbers
        }

        if n > 5 {
            break; // Stop once n exceeds 5
        }

        println!("Odd number: {n}");
    }
}
Odd number: 1
Odd number: 3
Odd number: 5

Why Rust Has loop Instead of while true

In many languages, an unconditional loop is written while (true) { ... }. While while true is syntactically valid in Rust, the compiler's static control-flow analyzer treats loop and while true fundamentally differently.

For while <condition>, the compiler assumes that the condition could evaluate to false on the very first check (without inspecting whether the condition is the literal true). For loop, the compiler knows with certainty that the loop body is guaranteed to execute and that control can only leave the loop via an explicit break, return, or panic.

This guarantee enables two capabilities that while true cannot provide:

1. Definite Variable Initialization

If a variable is declared before a loop and assigned a value before break, the compiler knows the variable is always initialized after the loop:

fn main() {
    let result: i32;

    loop {
        result = 42;
        break;
    }

    println!("result = {result}");
}
result = 42

Replacing loop with while true in the exact same program fails to compile because the compiler assumes a while loop might execute zero times, leaving result uninitialized:

// ❌ does not compile
fn main() {
    let result: i32;

    while true {
        result = 42;
        break;
    }

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

error[E0381]: used binding `result` isn't initialized
 --> src/main.rs:9:24
  |
2 |     let result: i32;
  |         ------ binding declared here but left uninitialized
3 |
4 |     while true {
  |           ---- if this `while` condition is `false`, `result` is not initialized
...
9 |     println!("result = {result}");
  |                        ^^^^^^^^ `result` used here but it isn't initialized

error: aborting due to 1 previous error; 1 warning emitted

Note that rustc also includes a built-in lint (#[warn(while_true)]) that explicitly advises replacing while true with loop.

2. Diverging Loops and the Never Type (!)

A loop that contains no break statements can never finish normally. Its type is the never type (!), which coerces into any type required by the surrounding context.

This allows an infinite event loop or server loop—or a loop that only exits via return—to serve as the body of a function returning any type:

fn find_first_multiple(factor: u32, threshold: u32) -> u32 {
    let mut current = factor;

    // Even though the function returns `u32` and there is no tail expression
    // after the loop, `rustc` knows this `loop` never falls through!
    loop {
        if current > threshold {
            return current;
        }
        current += factor;
    }
}

fn main() {
    println!("First multiple of 7 above 50: {}", find_first_multiple(7, 50));
}
First multiple of 7 above 50: 56

Variable Scope Across Iterations

The braces { ... } of a loop form a lexical block scope that starts fresh on every iteration and ends when the iteration completes (or when continue or break is executed).

  • A variable declared inside the loop body is created anew on each iteration and dropped at the end of that iteration.
  • A variable whose state must persist across iterations must be declared outside (before) the loop.
fn main() {
    let mut outer_total = 0;

    loop {
        // `per_iteration` is created fresh on every pass through the loop
        let per_iteration = 10;
        outer_total += per_iteration;

        if outer_total >= 30 {
            break;
        }
    }

    println!("outer_total = {outer_total}");
}
outer_total = 30

Unreachable Code Detection

Because rustc tracks that a loop without a break never terminates, any statement placed after an unbroken loop is flagged by the unreachable_code warning:

fn main() {
    let mut ticks = 0;

    loop {
        ticks += 1;
        if ticks == 2 {
            return; // Exits `main` directly; never breaks out of the loop
        }
    }

    println!("This line can never execute");
}
warning: unreachable statement
  --> src/main.rs:11:5
   |
 4 | /     loop {
 5 | |         ticks += 1;
 6 | |         if ticks == 2 {
 7 | |             return;
 8 | |         }
 9 | |     }
   | |_____- any code following this expression is unreachable
10 |
11 |       println!("This line can never execute");
   |       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unreachable statement
   |
   = note: `#[warn(unreachable_code)]` on by default