loop ist die einfachste — und gleichzeitig leistungsfähigste — Schleifen-Form in Rust. Ohne Bedingung am Kopf, ohne Iterator, läuft sie bis sie per break oder return verlassen wird. Das Besondere: nur loop erlaubt break <wert>, also einen Wert beim Verlassen zurückzugeben. Damit ist loop die richtige Wahl für Polling-Loops, Retry-Logik, Event-Verarbeitung und überhaupt alle Pattern, bei denen die Abbruch-Bedingung erst im Body sichtbar wird.

Grundform

Rust Endlos mit break
fn main() {
    let mut zaehler = 0;

    loop {
        zaehler += 1;
        if zaehler == 5 {
            break;
        }
    }

    println!("Zähler: {zaehler}");      // 5
}

loop ohne break läuft für immer — der Compiler gibt ihm in diesem Fall den Rückgabetyp ! (Never). Eine loop-Schleife, die nirgends break, return oder panic! enthält, terminiert nicht.

break mit Wert

Das macht loop einzigartig:

Rust break liefert Wert
fn main() {
    let mut zaehler = 0;

    let resultat = loop {
        zaehler += 1;
        if zaehler == 10 {
            break zaehler * 2;
        }
    };

    println!("{resultat}");        // 20
}

break <wert> macht aus der ganzen loop-Expression einen Wert. Damit kann eine Schleife direkt einer Bindung zugewiesen werden — kein Hilfs-mut außerhalb nötig.

Wichtig: Nur loop hat diese Form. while und for haben sie nicht — ihre Rückgabe ist immer ().

Typ aller break-Wert muss konsistent sein

Rust Typ-Konsistenz
// ok — alle break liefern i32
let n = loop {
    if cond_a() { break 1; }
    if cond_b() { break 2; }
};

// Fehler — Typ-Konflikt zwischen Branches
// let x = loop {
//     if cond { break 5; }
//     break "fünf";
// };
# fn cond_a() -> bool { true }
# fn cond_b() -> bool { false }
# let cond = true;

Wann loop, wann while oder for?

SituationWahl
Bekannte Sequenz / Iteratorfor x in ...
Bedingung am Schleifen-Anfang prüfbarwhile cond
Bedingung erst im Body sichtbar (z. B. nach Berechnung)loop mit break
Schleife muss einen Wert zurückgebenloop mit break <wert>
Endlos-Service (Daemon)loop ohne break

Klassisches Beispiel für „Bedingung erst im Body sichtbar": Retry-Logik, bei der das Ergebnis erst nach dem Versuch klar ist.

loop ohne break — das Never-Type

Eine loop-Schleife ohne Ausstieg hat Typ !:

Rust Server-Daemon
fn server() -> ! {
    loop {
        // Handle incoming requests
    }
}

Da der Compiler weiß: hier wird nie zurückgekehrt, darf der Rückgabetyp ! sein (siehe Unit-und-Never-Artikel).

Praxis: loop in echtem Code

Retry mit exponentiellem Backoff

Rust HTTP-Retry
use std::thread::sleep;
use std::time::Duration;

fn lade_mit_retry(url: &str) -> Result<String, &'static str> {
    let mut versuch = 0;
    let max_versuche = 5;

    loop {
        match hole(url) {
            Ok(body) => break Ok(body),                   // Erfolg: loop liefert Ok
            Err(e) if versuch >= max_versuche => break Err(e),   // zu viele Versuche
            Err(_) => {
                versuch += 1;
                let wartezeit = Duration::from_millis(100 * 2u64.pow(versuch));
                sleep(wartezeit);
            }
        }
    }
}
# fn hole(_: &str) -> Result<String, &'static str> { Err("fail") }

loop ist hier ideal, weil das Ergebnis erst nach dem Versuch bekannt ist. break Ok(body) und break Err(e) liefern verschiedene Result-Werte aus derselben Schleife.

User-Input bis korrekt

Rust Interaktive Eingabe
use std::io::{self, BufRead};

fn frage_zahl(prompt: &str) -> i32 {
    let stdin = io::stdin();
    loop {
        println!("{prompt}");
        let mut line = String::new();
        if stdin.lock().read_line(&mut line).is_err() {
            continue;
        }
        match line.trim().parse() {
            Ok(n) => break n,
            Err(_) => println!("Keine gültige Zahl, nochmal."),
        }
    }
}

Klassisches CLI-Pattern: bis der User eine gültige Eingabe macht, weiterfragen. Das break n gibt den geparsten Wert direkt aus der Funktion zurück.

Polling auf Channel

Rust Channel-Polling
use std::sync::mpsc::Receiver;
use std::time::Duration;

enum Cmd { Verarbeite(String), Beenden }

fn worker(rx: Receiver<Cmd>) {
    loop {
        match rx.recv_timeout(Duration::from_secs(1)) {
            Ok(Cmd::Verarbeite(daten)) => {
                println!("Verarbeite: {daten}");
            }
            Ok(Cmd::Beenden) => {
                println!("Worker beendet.");
                break;
            }
            Err(_) => {
                // Timeout — Heartbeat, weiter
            }
        }
    }
}

Worker-Threads laufen oft in loops, die per Beendigungs-Signal aus einem Channel terminieren. Der Cmd::Beenden-Branch ist die einzige break-Stelle — alles andere lässt den Loop weiterlaufen.

Binäre Suche „von Hand"

Rust Bisection
fn finde<F: Fn(i64) -> bool>(min: i64, max: i64, ok: F) -> i64 {
    let mut lo = min;
    let mut hi = max;
    loop {
        if lo + 1 >= hi { break lo; }
        let mitte = (lo + hi) / 2;
        if ok(mitte) {
            lo = mitte;
        } else {
            hi = mitte;
        }
    }
}

fn main() {
    // Finde das größte i mit i*i <= 100
    let n = finde(0, 1000, |i| i * i <= 100);
    assert_eq!(n, 10);
}

Hier ist break lo die Wert-Rückgabe der Schleife. Eleganter als ein Hilfs-mut-Result-Bindung außerhalb.

Event-Loop in einer Game-/UI-Anwendung

Rust Game-Loop-Skeleton
struct State { running: bool, ticks: u64 }

fn run_game() {
    let mut state = State { running: true, ticks: 0 };

    while state.running {
        // ... Event-Handling ...
        state.ticks += 1;
        if state.ticks > 100 { state.running = false; }
    }
    // Oder mit loop und break:
}

fn run_game_v2() {
    let mut state = State { running: true, ticks: 0 };
    loop {
        if !state.running { break; }
        state.ticks += 1;
        if state.ticks > 100 { state.running = false; }
    }
}

Beide Stile sind gültig. while ist hier vielleicht einen Tick lesbarer — solche Entscheidungen folgen oft persönlicher oder Team-Konvention.

Worker-Pool mit Job-Annahme

Rust Job-Aufnahme
use std::sync::Arc;
use std::sync::Mutex;

struct JobQueue {
    offen: Mutex<Vec<String>>,
}

fn worker(queue: Arc<JobQueue>) {
    loop {
        let job = {
            let mut offen = queue.offen.lock().unwrap();
            if offen.is_empty() {
                break;
            }
            offen.remove(0)
        };
        println!("Verarbeite: {job}");
    }
}

loop mit innerem Scope-Block: erst Lock holen, prüfen, Job nehmen, Lock loslassen, dann verarbeiten. Sehr typisches Concurrency-Pattern — Mutex-Guard lebt nur im inneren Block, danach kann ohne Lock weitergearbeitet werden.

Besonderheiten

loop ist die einzige Schleife mit break .

while und for haben immer Typ () — sie können einen Wert nicht zurückgeben. Wenn du also „Schleife mit Resultat-Wert" willst, ist loop die richtige Wahl.

loop ohne break hat Typ !.

Eine wirklich endlose Schleife terminiert nie — der Compiler gibt der Expression Typ !. Damit lässt sich ein endloser loop als Funktions-Body einer -> !-Funktion einsetzen (Server-Daemons, Embedded-Main-Loops).

continue in loop springt zum Schleifen-Anfang.

Im Gegensatz zu while oder for gibt es keine Bedingung am Anfang — continue springt direkt wieder zur ersten Anweisung im Body. Beispiel: in Retry-Schleifen, wo das Backoff-Sleep am Ende des Body steht, ist continue der Weg, dieses Sleep zu überspringen.

break ohne Wert in einer Wert-liefernden Schleife — Compile-Fehler.

Wenn andere Stellen break <wert> rufen, müssen alle break einen Wert haben (oder durch Coercion über ! passen). Ein nacktes break; in einer ansonsten Wert-liefernden Schleife produziert einen Type-Mismatch.

loop wird oft mit match kombiniert.

Klassisches Pattern: loop { match recv() { Ok(x) => break x, Err(_) => continue } }. Die match-Arme entscheiden: weiter (continue) oder mit Wert raus (break x). Sehr lesbar für Polling-Logik.

Labels erweitern loop auf verschachtelte Strukturen.

Bei verschachtelten Schleifen kannst du mit 'name: loop { ... break 'name <wert>; } aus einer äußeren Schleife mit Wert ausbrechen. Mehr im break/continue-Artikel.

loop { } ist nicht „busy wait“-frei.

Ein loop ohne sleep, recv oder I/O verbrennt eine ganze CPU-Kerne. In Concurrency-Code immer mit Backoff oder Blocking-Operationen kombinieren. Tools wie tokio::select! lösen das in async-Code idiomatisch.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Kontrollfluss

Zur Übersicht