Eine der wichtigsten Eigenschaften des Rust-match-Konstrukts ist seine Exhaustiveness-Garantie: der Compiler verlangt, dass dein match jede mögliche Variante des gematchten Typs behandelt. Vergisst du eine, ist das Compile-Fehler, kein Runtime-Bug. Wenn das Enum später um eine Variante erweitert wird, weist der Compiler dich überall darauf hin. Diese eine Garantie eliminiert eine komplette Bug-Klasse aus deinem Code. Dieser Artikel zeigt, wie die Prüfung funktioniert, was der _-Wildcard-Arm bewirkt, wie das #[non_exhaustive]-Attribut in Public-APIs funktioniert und welche typischen Stolperfallen man kennen sollte.

Hinweis zu &'static str: Manche Beispiele geben einen &'static str zurück — also eine Referenz auf einen Text, der für die gesamte Programm-Laufzeit gültig ist (typisch ein String-Literal aus dem Code). Die 'static-Notation ist eine Lifetime, die im Lifetimes-Kapitel ausführlich erklärt wird; hier reicht das mentale Bild „Verweis auf einen festen Text".

Die Exhaustiveness-Garantie

Rust Exhaustive match
enum Status {
    Aktiv,
    Inaktiv,
    Geloescht,
}

fn beschreibe(s: &Status) -> &'static str {
    match s {
        Status::Aktiv => "Status: aktiv",
        Status::Inaktiv => "Status: inaktiv",
        Status::Geloescht => "Status: gelöscht",
    }
}

Wenn du eine Variante vergisst, gibt es einen klaren Compile-Fehler:

Rust Fehlender Arm
# enum Status { Aktiv, Inaktiv, Geloescht }
fn beschreibe(s: &Status) -> &'static str {
    match s {
        Status::Aktiv => "aktiv",
        Status::Inaktiv => "inaktiv",
        // Fehler: pattern `&Status::Geloescht` not covered
    }
}

Diagnose:

Rust rustc-Output
error[E0004]: non-exhaustive patterns: `&Status::Geloescht` not covered
 --> src/main.rs:4:11
  |
4 |     match s {
  |           ^ pattern `&Status::Geloescht` not covered
  |
note: `Status` defined here
 --> src/main.rs:1:6

Die Diagnose ist nicht nur eine generische „bitte alle Varianten behandeln"-Warnung — sie nennt genau die fehlenden Patterns, mit Verweis auf die Definition des Enums. Bei drei Varianten mag das trivial wirken, aber bei einem Enum mit zwölf Varianten und einer komplexen Funktion mit mehreren match-Statements ist diese präzise Diagnose ein echter Lebensretter.

Die wirkliche Kraft zeigt sich, wenn das Enum später erweitert wird. Fügst du eine neue Variante Status::Pending hinzu, weist dich der Compiler in jedem match in der ganzen Codebase darauf hin, dass diese neue Variante nicht behandelt wird. Eine ganze Klasse von Bugs („vergessen, die neue Variante zu handhaben") ist ausgeschlossen. In großen Codebasen mit vielen Enums ist das ein zentraler Stabilitäts-Faktor.

Der Wildcard-Arm _

Wenn du nicht alle Varianten explizit behandeln willst:

Rust Mit Wildcard
# enum Status { Aktiv, Inaktiv, Geloescht }
fn ist_aktiv(s: &Status) -> bool {
    match s {
        Status::Aktiv => true,
        _ => false,         // alle anderen Varianten
    }
}

Der _-Arm fungiert als Catch-all: er matcht alles, was nicht von vorherigen Armen abgedeckt wurde. Damit wird das match automatisch exhaustive — der Compiler ist zufrieden, egal welche Varianten du übersprungen hast.

Das ist syntaktisch praktisch (eine Zeile statt zehn) und semantisch oft korrekt — etwa wenn dich nur eine spezifische Variante interessiert und alle anderen gleich behandelt werden sollen. Bei ist_aktiv ist das offensichtlich: aktiv = true, alles andere = false.

Vorsicht: _ versteckt neue Varianten

Wenn du später eine neue Variante zum Enum hinzufügst:

Rust Neue Variante
enum Status {
    Aktiv,
    Inaktiv,
    Geloescht,
    Gesperrt,         // NEU
}

fn ist_aktiv(s: &Status) -> bool {
    match s {
        Status::Aktiv => true,
        _ => false,         // fängt Gesperrt — aber soll es das?
    }
}

Hier liegt die Schattenseite des Wildcards: er versteckt vor dem Compiler, dass neue Varianten existieren könnten. Wenn jemand Status::Gesperrt hinzufügt, weist dich der Compiler nicht darauf hin, dass ist_aktiv jetzt auch diesen Fall behandeln muss — der _-Arm fängt ihn stillschweigend und behandelt ihn als „nicht aktiv". Ob das semantisch richtig ist, weiß nur der ursprüngliche Entwickler.

Die Empfehlung lautet daher: explizit auflisten, wo möglich. Der Compiler ist dein Freund — lass ihn dir bei jeder neuen Variante mitteilen, dass du nochmal über die Semantik nachdenken musst. Nur bei sehr breiten Enums (etwa io::ErrorKind mit 40+ Varianten) ist _ pragmatisch. Auch dann ist ein Kommentar wie _ => /* siehe handler::all_other_errors */ hilfreich, um die Absicht zu dokumentieren.

Strukturierte Wildcard mit Binding

Wenn du wissen willst, was unter dem _ lief:

Rust Catch-all mit Name
enum Cmd {
    Start,
    Stop,
    Pause,
    Custom(String),
}

fn handle(c: &Cmd) {
    match c {
        Cmd::Start => println!("Start"),
        Cmd::Stop => println!("Stop"),
        anderer => println!("Andere: {:?}", anderer_name(anderer)),
    }
}
# fn anderer_name(c: &Cmd) -> &'static str { "x" }

Statt eines anonymen _ kannst du einen Namen im Catch-all-Arm verwenden — etwa anderer. Das bindet die nicht-spezifisch-gematchte Variante an diesen Namen, und du kannst sie im Arm-Body weiter verarbeiten. Sehr nützlich für Logging der unerwarteten Werte: statt nur „andere Variante" zu loggen, kannst du den tatsächlichen Wert mit {anderer:?} ausgeben.

Das ist auch eine schöne Defensiv-Programmierungs-Variante: deine Hauptlogik behandelt die erwarteten Varianten explizit, der Catch-all loggt alles Unerwartete für späteres Debugging. Bei Production-Code, der mit dynamischen Daten umgeht (Parser, Protokoll-Handler, externe APIs), ist dieser Pattern Gold wert.

Exhaustiveness auf Daten

Die Prüfung gilt nicht nur für Varianten, sondern auch für Daten innerhalb der Variante:

Rust Datenprüfung
fn klassifiziere(n: i32) -> &'static str {
    match n {
        0 => "null",
        1..=9 => "klein",
        10..=99 => "mittel",
        // Compiler-Fehler: nicht alle i32-Werte abgedeckt
        // (z. B. negative, große)
    }
}

Bei numerischen Typen ist _ fast immer nötig — du kannst nicht alle 4 Milliarden i32-Werte auflisten:

Rust Mit Wildcard
fn klassifiziere(n: i32) -> &'static str {
    match n {
        0 => "null",
        1..=9 => "klein",
        10..=99 => "mittel",
        _ => "anderes",         // alle übrigen
    }
}

#[non_exhaustive] — vorwärtskompatible Enums

Library-Crates wollen oft die Möglichkeit offen halten, in Zukunft neue Varianten hinzuzufügen, ohne dass Konsumenten brechen:

Rust non_exhaustive
#[non_exhaustive]
pub enum HttpError {
    BadRequest,
    Unauthorized,
    NotFound,
    ServerError,
}

Im Library-Crate: ganz normal nutzbar.

Im Konsumenten-Crate:

Rust Konsument-Code
# mod externe_lib {
#     #[non_exhaustive]
#     pub enum HttpError { BadRequest, Unauthorized, NotFound, ServerError }
# }
use externe_lib::HttpError;

fn beschreibe(e: HttpError) -> &'static str {
    match e {
        HttpError::BadRequest => "400",
        HttpError::Unauthorized => "401",
        HttpError::NotFound => "404",
        HttpError::ServerError => "500",
        _ => "anderer Fehler",    // PFLICHT — Compiler verlangt es
    }
}

Selbst wenn du aktuell alle bekannten Varianten abdeckst, verlangt der Compiler beim Konsumenten einen _-Arm. Damit kann die Library neue Varianten hinzufügen, ohne den Konsumenten zu brechen.

Wann nutzen: in jedem Public-API-Enum, das in Zukunft erweitert werden könnte. Standard-Pattern in stabilen Stdlib- und Crates-Bibliotheken.

Guards und Exhaustiveness

Match-Arme können Guards mit if-Bedingungen haben:

Rust Guards
fn beschreibe(n: i32) -> &'static str {
    match n {
        x if x < 0 => "negativ",
        0 => "null",
        x if x > 100 => "groß",
        _ => "klein bis mittel",
    }
}

Wichtig: Guards werden vom Exhaustiveness-Checker als nicht alle Fälle abdeckend behandelt. Selbst wenn deine Guards logisch alle Werte abdecken, verlangt der Compiler trotzdem einen Catch-All-Arm.

Rust Guard-Falle
fn beschreibe(n: i32) -> &'static str {
    match n {
        x if x < 0 => "negativ",
        x if x == 0 => "null",
        x if x > 0 => "positiv",
        // Compiler beschwert sich — Guards sind nicht „bewiesen exhaustive"
        // _ => unreachable!(),       // Lösung
    }
}

Der Compiler kann die Guard-Bedingungen nicht analysieren. Lösung: ein finaler _ => unreachable!()-Arm.

Or-Patterns

Mehrere Patterns mit | kombiniert:

Rust Or-Pattern
fn ist_inaktiv(s: &Status) -> bool {
    match s {
        Status::Inaktiv | Status::Geloescht => true,
        Status::Aktiv => false,
    }
}
# enum Status { Aktiv, Inaktiv, Geloescht }

Status::Inaktiv | Status::Geloescht matcht beide Varianten mit derselben Aktion. Reduziert Boilerplate erheblich.

Praxis: Exhaustive Match im echten Code

Domain-Status

Rust Order-Status
pub enum OrderStatus {
    Erstellt,
    Bezahlt,
    Versendet,
    Geliefert,
    Storniert,
}

pub fn naechster_status(aktuell: OrderStatus) -> Option<OrderStatus> {
    match aktuell {
        OrderStatus::Erstellt => Some(OrderStatus::Bezahlt),
        OrderStatus::Bezahlt => Some(OrderStatus::Versendet),
        OrderStatus::Versendet => Some(OrderStatus::Geliefert),
        OrderStatus::Geliefert => None,         // Endzustand
        OrderStatus::Storniert => None,
    }
}

Alle 5 Varianten explizit. Wenn später Zurueckgesandt dazukommt, weist der Compiler darauf hin.

HTTP-Status-Code-Klassifizierung

Rust HTTP-Klassen
pub fn klasse(code: u16) -> &'static str {
    match code {
        100..=199 => "1xx Informational",
        200..=299 => "2xx Success",
        300..=399 => "3xx Redirect",
        400..=499 => "4xx Client Error",
        500..=599 => "5xx Server Error",
        _ => "Unknown",
    }
}

Range-Patterns plus _ für alle anderen. Numerischer Match braucht fast immer Wildcard.

Event-Handling mit Or-Pattern

Rust Event-Dispatch
pub enum Event {
    Click(u32, u32),
    DoubleClick(u32, u32),
    KeyDown(char),
    KeyUp(char),
    MouseMove(u32, u32),
    Resize(u32, u32),
}

pub fn ist_pointer_event(e: &Event) -> bool {
    match e {
        Event::Click(_, _) | Event::DoubleClick(_, _) | Event::MouseMove(_, _) => true,
        Event::KeyDown(_) | Event::KeyUp(_) => false,
        Event::Resize(_, _) => false,
    }
}

Or-Patterns gruppieren ähnliche Events. Wenn neue Event-Typen kommen, sieht der Compiler die fehlende Behandlung.

Mehr-Varianten-Enum mit non_exhaustive

Rust Library-Error
#[non_exhaustive]
pub enum ApiError {
    BadRequest,
    Unauthorized,
    Forbidden,
    NotFound,
    Conflict,
    ServerError,
    ServiceUnavailable,
}

pub fn http_status(e: &ApiError) -> u16 {
    match e {
        ApiError::BadRequest => 400,
        ApiError::Unauthorized => 401,
        ApiError::Forbidden => 403,
        ApiError::NotFound => 404,
        ApiError::Conflict => 409,
        ApiError::ServerError => 500,
        ApiError::ServiceUnavailable => 503,
        _ => 500,        // Pflicht wegen non_exhaustive
    }
}

Library-Pattern: non_exhaustive plus _-Arm im Konsumenten — zukunftssicher.

State-Machine-Übergänge

Rust Connection-State
pub enum ConnState {
    Inaktiv,
    Verbindet,
    Authentifiziert,
    Aktiv,
    Geschlossen,
}

pub fn darf_senden(s: &ConnState) -> bool {
    match s {
        ConnState::Aktiv => true,
        ConnState::Inaktiv | ConnState::Verbindet
        | ConnState::Authentifiziert | ConnState::Geschlossen => false,
    }
}

Or-Pattern alle Nicht-Aktiv-States. Wenn neuer State dazukommt, muss explizit entschieden werden, ob er senden darf.

Bei Daten: explizite Behandlung

Rust Mit Daten
pub enum LogLevel {
    Debug,
    Info,
    Warn,
    Error,
    Custom(u32),
}

pub fn level_zahl(l: LogLevel) -> u32 {
    match l {
        LogLevel::Debug => 0,
        LogLevel::Info => 1,
        LogLevel::Warn => 2,
        LogLevel::Error => 3,
        LogLevel::Custom(n) => n,
    }
}

Custom(n) bindet den inneren Wert. Wenn Custom-Varianten weggelassen werden, gibt's einen Compile-Fehler.

Nested Match

Rust Verschachtelt
pub enum DbResult {
    Erfolg(Vec<String>),
    ZeitUeberschritten,
    Fehler { code: i32, nachricht: String },
}

pub fn beschreibe(r: &DbResult) -> String {
    match r {
        DbResult::Erfolg(reihen) if reihen.is_empty() => "leer".to_string(),
        DbResult::Erfolg(reihen) => format!("{} Reihen", reihen.len()),
        DbResult::ZeitUeberschritten => "Timeout".to_string(),
        DbResult::Fehler { code, nachricht } => format!("[{code}] {nachricht}"),
    }
}

Guards plus normale Patterns gemischt. Der Compiler erkennt, dass die beiden Erfolg-Arme zusammen alle Erfolg-Fälle abdecken (leer + nicht-leer).

Mit unreachable! als Sicherheit

Rust unreachable
fn signum(n: i32) -> i32 {
    match n {
        x if x > 0 => 1,
        x if x < 0 => -1,
        0 => 0,
        _ => unreachable!("alle Fälle abgedeckt"),
    }
}

unreachable!() panickt zur Laufzeit, falls je erreicht — was Logik-Bugs sofort sichtbar macht. Trotzdem erfüllt es exhaustive-Anforderung.

Häufige Stolperfallen

_ versteckt neue Varianten.

Wenn du _ => ... schreibst, fängt er auch zukünftig hinzugefügte Varianten — möglicherweise mit falscher Semantik. Bei deinen eigenen Enums (vollständig sichtbar): explizit auflisten. Compiler-Warnings sind dein Freund.

Bei #[non_exhaustive]-Enums ist _ Pflicht.

Library-Konsumenten müssen einen _-Arm haben — sonst Compile-Fehler bei Updates. Das ist die Verträge des Library-Autors: „ich behalte mir vor, Varianten hinzuzufügen, du musst sie tolerieren".

Guards machen Exhaustiveness nicht entscheidbar.

match n { x if x &gt; 0 =&gt; ..., x if x &lt; 0 =&gt; ..., 0 =&gt; ... } ist logisch komplett, aber der Compiler kann das nicht verifizieren. Lösung: _ => unreachable!() als letzter Arm.

Range-Patterns bei Integern brauchen meist _.

match n: i32 { 0..=99 =&gt; ..., 100..=999 =&gt; ... } ist nicht exhaustive — negative Werte und 1000+ fehlen. Wildcard nötig.

Match auf Or-Patterns ist sehr expressiv.

A | B | C => ... für gemeinsame Behandlung mehrerer Varianten. Beim Refactoring (eine Variante umbenennen): Or-Patterns geben dem Compiler die Chance, alle Stellen zu finden.

todo!() und unimplemented!() als Platzhalter.

Während der Entwicklung: ein Arm => todo!() macht das match exhaustive, aber panickt zur Laufzeit. Du kannst weiterprogrammieren und vergisst die Stelle nicht.

Reihenfolge der Arme ist relevant.

Der Compiler probiert die Arme von oben nach unten — der erste passende gewinnt. Spezifische Patterns kommen vor allgemeineren. Some(0) =&gt; ... vor Some(_) =&gt; .... Falsche Reihenfolge führt zu Dead-Code-Warnings.

Match auf Referenzen automatisch.

match &enum_value matcht auf Patterns wie Variant(x) — der & wird automatisch via „match ergonomics" eingefügt. x wird zur Referenz. Sehr bequem, manchmal aber irritierend (Wert vs. Referenz im Pattern).

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Enums & Pattern Matching

Zur Übersicht