Wenn nur ein bestimmtes Pattern interessiert (häufig: Some(x) aus einem Option oder Ok(x) aus einem Result), wäre ein vollständiges match mit einem ignorierten _-Arm verschwendet. Genau dafür gibt es if let, while let und seit Rust 1.65 let else — drei verwandte Konstrukte, die Pattern-Matches kompakt machen. Sie sind aus modernem Rust nicht mehr wegzudenken und ersetzen oft tief verschachtelte match-Strukturen durch klare lineare Logik.

if let — Match nur einen Branch

Rust Vor und nach if let
let maybe: Option<i32> = Some(42);

// Mit vollständigem match — overkill für einen Branch
match maybe {
    Some(n) => println!("Wert: {n}"),
    None => {},          // Nichts zu tun
}

// Mit if let — kürzer
if let Some(n) = maybe {
    println!("Wert: {n}");
}

if let <pattern> = <expression> läuft den Body nur, wenn expression zu pattern matcht. Die im Pattern gebundenen Variablen (hier n) sind im Body verfügbar.

if let mit else

Rust if let / else
let maybe: Option<i32> = Some(42);

if let Some(n) = maybe {
    println!("Wert: {n}");
} else {
    println!("Kein Wert");
}

Das else läuft, wenn das Pattern nicht matcht. Praktisch wie ein zwei-armiger match — aber kürzer.

if let mit else-if

Rust Mehrere if let
enum Event { Login(String), Click(u32, u32), Idle }

fn beschreibe(e: &Event) -> String {
    if let Event::Login(name) = e {
        format!("Login von {name}")
    } else if let Event::Click(x, y) = e {
        format!("Klick bei ({x}, {y})")
    } else {
        "Idle".to_string()
    }
}

Mehrere if let-Bedingungen in Kette. Wenn es mehrere Patterns werden, ist ein vollständiger match oft lesbarer:

Rust Alternative mit match
# enum Event { Login(String), Click(u32, u32), Idle }
fn beschreibe(e: &Event) -> String {
    match e {
        Event::Login(name) => format!("Login von {name}"),
        Event::Click(x, y) => format!("Klick bei ({x}, {y})"),
        Event::Idle => "Idle".to_string(),
    }
}

Faustregel: 1 Pattern → if let. 2+ Patterns → match.

while let — solange Pattern matcht

Schon im while-Artikel behandelt, hier nochmal als Vertiefung:

Rust while let als Drain
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
    println!("{top}");
}
// 3, 2, 1

Pattern matcht → Schleife läuft. Pattern matcht nicht (hier: None aus leerem Stack) → Schleife endet.

let else — Early-Return-Pattern

Stable seit Rust 1.65. Das wichtigste Konstrukt dieser Familie.

Rust Klassischer Verschachtelungs-Code
fn lade_user(id_str: &str) -> Result<User, &'static str> {
    let id: u64 = match id_str.parse() {
        Ok(n) => n,
        Err(_) => return Err("ID keine Zahl"),
    };

    let user = match datenbank_lookup(id) {
        Some(u) => u,
        None => return Err("User nicht gefunden"),
    };

    // ... jetzt mit user weiterarbeiten
    Ok(user)
}
# struct User;
# fn datenbank_lookup(_: u64) -> Option<User> { None }

Diese Vor-1.65-Form ist verbose. Mit let else wird es:

Rust Mit let else
# struct User;
# fn datenbank_lookup(_: u64) -> Option<User> { None }
fn lade_user(id_str: &str) -> Result<User, &'static str> {
    let Ok(id) = id_str.parse::<u64>() else {
        return Err("ID keine Zahl");
    };

    let Some(user) = datenbank_lookup(id) else {
        return Err("User nicht gefunden");
    };

    // user ist hier direkt verfügbar — kein Indent, kein Hilfs-match
    Ok(user)
}

Die Magie: bei let else muss der else-Branch divergierend sein (return, panic, break, continue). Wenn das Pattern matcht, läuft normaler Code weiter — die gebundene Variable ist direkt im umliegenden Scope verfügbar, ohne extra Verschachtelung.

Regeln für let else

  • Das Pattern darf refutable sein (kann fehlschlagen).
  • Der else-Block muss zwingend divergent enden (Typ !): return, break, continue, panic!, todo!(), loop {}.
  • Die im Pattern gebundenen Variablen leben außerhalb der if-Klammer — im Rest des Scopes.

Patterns mit Guards

Auch if let und let else unterstützen komplexere Patterns:

Rust Verschachtelte Patterns
struct Konfig { db: Option<String>, port: Option<u16> }

fn pruefe(c: &Konfig) {
    if let Konfig { db: Some(url), port: Some(p) } = c {
        println!("DB={url} auf Port {p}");
    }
}

fn main() {
    let c = Konfig { db: Some("postgres://...".into()), port: Some(5432) };
    pruefe(&c);
}

Pattern auf Struct mit verschachtelten Options — beide müssen Some sein, dann sind url und p gebunden.

let-chains (Edition 2024+)

Seit Edition 2024 stable: let-Chains verkettet mit &&:

Rust let chains
# struct Conf { name: Option<String>, port: Option<u16> }
fn check(c: &Conf) {
    if let Some(name) = &c.name
        && let Some(port) = c.port
        && port > 1024
    {
        println!("{name} auf Port {port}");
    }
}

Mehrere Pattern-Matches plus boolesche Bedingungen, alle in einer Linie. Vor 2024 brauchte man verschachtelte if lets — jetzt geht es in einem Schritt.

Praxis: if let, let else, while let in echtem Code

Config-Validation mit let-else-Pipeline

Rust Linear ohne Indent
use std::env;

#[derive(Debug)]
struct Config {
    host: String,
    port: u16,
    workers: u32,
}

fn lade_config() -> Result<Config, String> {
    let Ok(host) = env::var("APP_HOST") else {
        return Err("APP_HOST fehlt".into());
    };
    let Ok(port_str) = env::var("APP_PORT") else {
        return Err("APP_PORT fehlt".into());
    };
    let Ok(port) = port_str.parse::<u16>() else {
        return Err("APP_PORT keine u16".into());
    };
    let Ok(workers) = env::var("APP_WORKERS").unwrap_or("4".into()).parse::<u32>() else {
        return Err("APP_WORKERS keine u32".into());
    };
    Ok(Config { host, port, workers })
}

Vier let elses in linearer Folge — kein verschachteltes match, keine Pyramid-of-Doom. Jeder Erfolgsfall bindet im normalen Scope, Fehler führen zum Return.

Result-Handling im Iterator

Rust Skip-on-Error in for-Loop
fn verarbeite_zeilen(zeilen: &[String]) {
    for zeile in zeilen {
        let Ok(zahl) = zeile.parse::<i32>() else {
            eprintln!("'{zeile}' übersprungen");
            continue;
        };
        println!("{}", zahl * 2);
    }
}

let else mit continue als divergenter Branch. „Wenn keine Zahl, dann nächste Iteration". Sehr lesbar gegenüber einem if let / else { continue }.

Option-Chain mit if let

Rust Optional-Daten
struct Profil { name: String, telefon: Option<String> }
struct Account { profil: Option<Profil> }

fn drucke_telefon(a: &Account) {
    if let Some(p) = &a.profil {
        if let Some(t) = &p.telefon {
            println!("Telefon: {t}");
        }
    }
}
// Mit Edition 2024 let chains:
fn drucke_telefon_v2(a: &Account) {
    if let Some(p) = &a.profil
        && let Some(t) = &p.telefon
    {
        println!("Telefon: {t}");
    }
}

Bei tieferer Verschachtelung lohnen sich let-chains besonders.

Event-Processor mit if let

Rust Event-Dispatch
enum Event { Click(u32, u32), KeyDown(char), Resize(u32, u32) }

fn behandle(events: &[Event]) {
    for e in events {
        if let Event::Click(x, y) = e {
            println!("Click bei ({x}, {y})");
            // ... handle click ...
        }
        // andere Events ignorieren
    }
}

Wenn nur eine Event-Art interessiert (etwa in einem fokussierten Handler), ist if let klarer als ein vollständiger match mit _ => {}.

Queue-Drain mit while let

Rust Drain-Pattern
use std::collections::VecDeque;

fn verarbeite_alle(q: &mut VecDeque<String>) {
    while let Some(msg) = q.pop_front() {
        println!("Verarbeite: {msg}");
    }
}

Idiomatischer „Container leeren und arbeiten"-Pattern. pop_front gibt Some solange was da ist, dann None — perfekt für while let.

Header-Parser mit if let

Rust HTTP-Header-Parser
fn content_length(headers: &[(String, String)]) -> Option<usize> {
    for (k, v) in headers {
        if k.eq_ignore_ascii_case("content-length") {
            if let Ok(n) = v.parse::<usize>() {
                return Some(n);
            }
        }
    }
    None
}

if let Ok(n) = v.parse::<usize>() — nimm den geparsten Wert wenn's klappt, sonst weitersuchen.

Resource-Lookup mit early return

Rust Cache-Lookup
use std::collections::HashMap;

fn finde_oder_baue(cache: &mut HashMap<String, String>, key: &str) -> &str {
    if cache.contains_key(key) {
        // Bereits da — gib aus dem Cache zurück
        return cache.get(key).unwrap();
    }

    // Nicht im Cache — neu berechnen
    let wert = format!("berechnet-{key}");
    cache.insert(key.to_owned(), wert);
    cache.get(key).unwrap()
}

(In echtem Code würde man entry() nutzen — hier nur zur Demo des Pattern-Stils.)

Argument-Validierung mit let else

Rust CLI-Validator
fn process(args: &[String]) -> Result<(), &'static str> {
    let [_program, cmd, arg] = args else {
        return Err("Aufruf: programm <cmd> <arg>");
    };

    match cmd.as_str() {
        "lese" => println!("Lese: {arg}"),
        "schreibe" => println!("Schreibe: {arg}"),
        _ => return Err("Unbekanntes Kommando"),
    }
    Ok(())
}

let [_program, cmd, arg] = args else — Slice-Pattern mit fester Länge. Wenn args nicht genau 3 Elemente hat, geht's in den else-Block. Sehr kompakte CLI-Arg-Validierung.

Besonderheiten

let else ist ein Game-Changer für Early-Return-Style.

Vor Rust 1.65 hatten Funktionen mit vielen Validierungs-Schritten entweder tiefe Indent-Pyramiden oder verbose match-Statements. Mit let else wird das ein lineares Stück Code, das Schritt für Schritt validiert und im Fehlerfall sofort raus springt.

Der else-Branch von let else MUSS divergent sein.

Typ ! — also return, break, continue, panic!, todo!, oder ein loop {} ohne break. Ein normaler Block, der einen Wert produzieren würde, ist ein Compile-Fehler. Das macht let else klar abgrenzbar von if let / else.

Patterns in if let dürfen refutable sein.

Im Gegensatz zu normalem let (das ein infallibles Pattern verlangt), darf if let ein Pattern haben, das fehlschlagen kann. Genau das ist sein Existenz-Grund: punktuell genau einen Fall greifen.

while let bei iter_mut-Bedingung bricht Borrow-Checker.

while let Some(x) = iter.next() {} mit iter = v.iter_mut() blockiert v für die ganze Schleife. Workarounds: for x in v.iter_mut() (funktioniert, weil for-Loops anders verarbeitet werden) oder pop()-basiertes Pattern, das nicht borrowt sondern entfernt.

let else ist nicht das gleiche wie ?-Operator.

? arbeitet auf Result<T, E> und gibt automatisch den Error im Fehler-Fall zurück. let else ist allgemeiner — funktioniert auf jedem Pattern (Option, Result, eigene Enums, Slices, ...) und lässt dich den Fehler-Pfad selbst schreiben.

let chains sind Edition 2024.

if let A && let B && cond ist erst seit Rust 1.85 / Edition 2024 stable. Wer auf älteren Versionen ist, muss verschachtelte if-lets schreiben. Mit cargo fix --edition lässt sich teilweise automatisch portieren.

if let mit mehreren Patterns geht (seit Rust 1.75).

if let Some(1) | Some(2) = x matcht beide. Praktisch für „eine Hand von Pattern-Varianten zusammen behandeln". Vor 1.75 musste man stattdessen ein match schreiben.

let else bindet Variablen im äußeren Scope, nicht in einem inneren Block.

Im Gegensatz zu if let Some(x) = ... ist let Some(x) = ... else so, dass x nach der Anweisung verfügbar ist (im normalen Funktions-Scope). Das macht den Code linear lesbar, statt verschachtelt.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Kontrollfluss

Zur Übersicht