let else ist eines der wichtigsten Sprach-Updates seit langem (stable seit Rust 1.65, November 2022). Es löst ein hartnäckiges Problem: Funktionen mit mehreren Validierungs-Schritten erzeugen ohne let else entweder tiefe Indent-Pyramiden oder verbose match-Statements. Mit let else wird daraus linearer Code — jeder Schritt validiert, im Fehlerfall verlässt man die Funktion sofort, im Erfolgsfall ist das gebundene Pattern direkt im umliegenden Scope nutzbar. Dieser Artikel zeigt die Syntax, die Regeln (Pattern muss refutable sein, else muss divergent enden) und die zentralen Anwendungs-Patterns.
Das Problem
Klassischer Validierungs-Code vor let else:
# struct User;
# fn datenbank_lookup(_: u64) -> Option<User> { None }
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"),
};
Ok(user)
}Funktioniert, ist aber verbose. Bei mehreren Schritten wird's noch unübersichtlicher.
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)
}Linearer, kein Indent-Anstieg, jeder Schritt validiert für sich. Das ist der Game-Changer.
Die Syntax
// let pattern = expression else { divergent_block };
let Some(n) = optional else {
return;
};
// n ist hier verfügbar.
# let optional: Option<i32> = None;Drei Bestandteile:
let pattern = expression— Pattern-Match wie bei normalemlet.else { ... }— wird ausgeführt, wenn das Pattern nicht matcht.- Der else-Block muss divergent sein — also
return,break,continue,panic!,loop {}, oder eine andere Expression mit Typ!.
Im Erfolgsfall ist das Pattern im äußeren Scope gebunden — anders als bei if let, wo die Bindung nur im Body lebt.
Regeln im Detail
Pattern muss refutable sein
let else macht nur Sinn mit Patterns, die fehlschlagen können. Sonst ist der else-Branch toter Code.
# let x: i32 = 42;
// Refutable — kann fehlschlagen:
let Some(n) = Some(42) else { return; };
let Ok(m) = Ok::<i32, ()>(42) else { return; };
// Infallible — Compile-Fehler:
// let (a, b) = (1, 2) else { return; };
// Lösung: einfaches let
let (a, b) = (1, 2);Bei infallible Patterns (Tupel, Struct ohne Enum-Variante) gibt's keinen else-Branch zu rechtfertigen.
Else-Block muss divergent sein
fn beispiel(x: Option<i32>) -> i32 {
// ok — return ist divergent
let Some(n) = x else { return -1; };
// ok — panic ist divergent
// let Some(n) = x else { panic!("Wert fehlt"); };
// ok — continue/break in einer Schleife
// let Some(n) = x else { continue; };
// NICHT ok — der Block gibt einen normalen Wert zurück
// let Some(n) = x else { 0 };
n
}Der else-Block muss den Funktions-Scope verlassen oder in einer Schleife continue/break machen. Ein normaler Wert ist Compile-Fehler.
Bindings im äußeren Scope
fn beispiel(x: Option<i32>) -> i32 {
let Some(n) = x else { return -1; };
// n ist HIER — im äußeren Scope — verfügbar.
n * 2
}Das ist der entscheidende Unterschied zu if let: bei if let Some(n) = ... ist n nur im Body sichtbar. Bei let else ist n im gesamten umliegenden Scope verfügbar.
Vergleich zu Alternativen
| Pattern | Verhalten bei Fehler | Bindung im äußeren Scope |
|---|---|---|
match | beliebig im jeweiligen Arm | nein |
if let / else | beliebig im else-Branch | nein (nur im if-Body) |
let else | muss divergent enden | ja |
?-Operator | early return mit Err/None | ja, aber nur für Result/Option |
let else füllt eine Lücke: wenn du nach dem Match weitermachen willst und der Fehlerfall einen frühen Ausstieg bedeutet.
Mehrere let else hintereinander
Der eigentliche Wert von let else zeigt sich, wenn mehrere Validierungs-Schritte aufeinander folgen:
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(format!("APP_PORT '{port_str}' keine u16"));
};
let workers: u32 = env::var("APP_WORKERS")
.unwrap_or("4".into())
.parse()
.map_err(|_| "APP_WORKERS keine u32")?;
Ok(Config { host, port, workers })
}Vier Validierungs-Schritte, alle linear. Ohne let else wäre das eine Pyramide oder fünf separate match-Statements.
let else mit Slice-Patterns
let else funktioniert mit allen refutable Patterns — einschließlich Slice-Patterns:
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 — matcht nur, wenn args exakt 3 Elemente hat. Sehr typisches CLI-Pattern.
let else mit komplexen Patterns
struct Account { profil: Option<Profil> }
struct Profil { name: String }
fn extrahiere_name(a: &Account) -> Option<&str> {
let Some(Profil { name, .. }) = &a.profil else {
return None;
};
Some(name.as_str())
}Verschachtelte Patterns mit let else — auch komplex destrukturieren in einem Schritt.
Praxis: let else im echten Code
Config-Laden mit voller Validierung
pub fn lade_aus_env() -> Result<u16, String> {
let Ok(s) = std::env::var("PORT") else {
return Err("PORT nicht gesetzt".into());
};
let Ok(port) = s.parse::<u16>() else {
return Err(format!("PORT '{s}' keine u16"));
};
if port < 1024 {
return Err("Port < 1024 ist privilegiert".into());
}
Ok(port)
}Drei Validierungs-Schritte. Jeder hat eine eigene Fehler-Meldung mit Kontext.
CLI-Argument-Validator
pub fn parse_args(args: &[String]) -> Result<(String, String), String> {
let [_program, cmd, arg] = args else {
return Err("Genau 2 Argumente: <cmd> <arg>".into());
};
if cmd.is_empty() {
return Err("Kommando darf nicht leer sein".into());
}
Ok((cmd.clone(), arg.clone()))
}Slice-Pattern für Argument-Anzahl, weitere Validierung mit klassischen Checks.
Skip-on-Error in Schleife
pub fn verarbeite_zeilen(zeilen: &[String]) {
for zeile in zeilen {
let Ok(n) = zeile.parse::<i32>() else {
eprintln!("Übersprungen: '{zeile}'");
continue;
};
println!("Verarbeite: {n}");
}
}continue als divergenter Branch — überspringt den Rest der Schleifen-Iteration.
Option-Field-Extraction
struct User { email: Option<String> }
pub fn domain_extrahieren(u: &User) -> Result<&str, &'static str> {
let Some(email) = &u.email else {
return Err("Email fehlt");
};
let Some(at_pos) = email.find('@') else {
return Err("Email hat kein @");
};
Ok(&email[at_pos + 1..])
}Drei Validierungs-Schritte, alle linear.
Composite Pattern Match
enum Response {
Ok { body: Vec<u8>, content_type: String },
Err(String),
}
pub fn extrahiere_text(r: Response) -> Result<String, String> {
let Response::Ok { body, content_type } = r else {
return Err("Response war Err".into());
};
if !content_type.starts_with("text/") {
return Err(format!("Kein Text-Type: {content_type}"));
}
String::from_utf8(body).map_err(|e| e.to_string())
}Enum-Destrukturierung mit let else — sehr lesbar, alle Felder im äußeren Scope verfügbar.
Combined mit ?
pub fn handle_request(input: &str) -> Result<i32, &'static str> {
// let else für „braucht eine bestimmte Form"
let Some((key, wert)) = input.split_once('=') else {
return Err("kein = gefunden");
};
// ? für Standard-Conversion
let zahl: i32 = wert.parse().map_err(|_| "kein i32")?;
// weitere Logik
Ok(zahl * key.len() as i32)
}Kombination aus let else (für Form-Validierung) und ? (für Type-Conversion). Beides linear, kein Indent.
State-Validierung
enum Verbindung { Inaktiv, Aktiv(String), Geschlossen }
pub fn sende(v: &Verbindung, msg: &str) -> Result<(), &'static str> {
let Verbindung::Aktiv(peer) = v else {
return Err("Nicht im aktiven Zustand");
};
println!("Sende '{msg}' an {peer}");
Ok(())
}State-Validation mit Enum-Pattern — Send nur im Aktiv-State erlaubt.
Mehrfaches Lookup
use std::collections::HashMap;
pub fn baue_url(config: &HashMap<String, String>) -> Result<String, &'static str> {
let Some(host) = config.get("host") else {
return Err("host fehlt");
};
let Some(scheme) = config.get("scheme") else {
return Err("scheme fehlt");
};
let port = config.get("port").map(|s| s.as_str()).unwrap_or("80");
Ok(format!("{scheme}://{host}:{port}"))
}Zwei Pflicht-Lookups, ein optionaler — Mix aus let else (Pflicht) und unwrap_or (Default).
Mit Block-Label
pub fn klassifiziere(input: &str) -> &'static str {
'bestimmen: {
let Some(first) = input.chars().next() else {
break 'bestimmen "leer";
};
if first.is_ascii_digit() {
break 'bestimmen "fängt mit Ziffer an";
}
"fängt mit Buchstabe an"
}
}let else mit break 'label. Block-Labels (seit Rust 1.65) als divergenter Ziel — perfekte Ergänzung zu let else.
Besonderheiten
let else ist Edition-übergreifend stable (seit Rust 1.65).
Eine der wichtigsten Sprach-Erweiterungen der letzten Jahre. Macht aus tiefen Indent-Pyramiden lineare Validierungs-Pipelines.
Else-Block MUSS divergent enden.
return, break, continue, panic!, todo!(), unimplemented!(), loop {} — alles mit Typ !. Ein normaler Wert oder () ist Compile-Fehler.
Bindings landen im äußeren Scope.
Anders als if let, bei dem die Bindung nur im Body lebt. Bei let else ist die im Pattern gebundene Variable im gesamten Funktions-/Block-Scope verfügbar. Das ist der Hauptvorteil.
Mehrere let else ergeben lineare Validierung.
Vier let else hintereinander = vier Validierungs-Schritte mit klarer Fehler-Diagnose pro Fall. Ohne let else wären das vier verschachtelte oder verbose match-Blöcke.
let else ist nicht der ?-Ersatz.
? automatisiert Result/Option-Propagation. let else ist allgemeiner — funktioniert auf jedem refutable Pattern, du schreibst den Fehler-Pfad selbst. Beide ergänzen sich.
Slice-Patterns in let else sind extrem nützlich für CLI.
let [_program, cmd, arg] = args else { return Err("..."); }; — exakte Argument-Anzahl plus Validierung in einem Schritt.
Mit Block-Labels für lokalen „Exit".
Innerhalb eines 'label: { ... }-Blocks kannst du let else mit break 'label value kombinieren — lokaler Exit ohne Funktion zu verlassen.
Komplexe Patterns funktionieren auch.
let Some(Profil { name, .. }) = &a.profil else { return None; }; — verschachtelte Destrukturierung in einem Schritt. Sehr expressiv.
Weiterführende Ressourcen
Externe Quellen
- Rust 1.65 Release Notes – let else
- Rust Reference – let statements
- RFC 3137 – let-else
- The Rust Book – Patterns