Error-Handling in Rust unterscheidet sich grundlegend von Exception-basierten Sprachen. Es gibt keine Exceptions, kein try/catch, keine schweigende Stack-Unwinding-Magie. Stattdessen sind Fehler Werte: eine Funktion, die scheitern kann, gibt ein Result<T, E> zurück; der Aufrufer muss explizit entscheiden, was zu tun ist. Für unrecoverable Programm-Zustände gibt es panic! als kontrolliertes Crashen. Mit dem ?-Operator wird Fehler-Propagation so kompakt wie in modernen try/catch-Sprachen — aber ohne deren versteckte Control-Flow-Probleme. Dieses Kapitel führt durch das gesamte Spektrum: von den Grundkonzepten bis zu produktiven Patterns mit thiserror und anyhow.

Die zwei Fehler-Kategorien

Rust unterscheidet klar zwischen zwei Arten von Problemen, die in einem Programm auftreten können — und behandelt sie mit unterschiedlichen Sprach-Konstrukten.

Recoverable Errors sind erwartete Fehler-Situationen, mit denen der Code umgehen muss. Eine Datei fehlt, eine Netzwerk-Verbindung bricht ab, ein Parser stößt auf ungültige Eingabe, eine Datenbank gibt einen Constraint-Verstoß zurück. All das sind keine Bugs — es sind reguläre Programm-Zustände, auf die der aufrufende Code reagieren können soll. Für diese Fälle gibt es Result<T, E>: der erfolgreiche Fall ist Ok(t), der Fehler Err(e), und der Compiler verlangt eine explizite Behandlung.

Unrecoverable Errors sind Zustände, die niemals auftreten sollten und auf die keine sinnvolle Reaktion möglich ist. Index-out-of-Bounds in einem Slice, Division durch Null in einem Integer, Verletzung einer internen Invariante in deinem eigenen Code. Diese Fälle deuten auf Bugs hin — die richtige Reaktion ist, das Programm sofort zu beenden und einen klaren Diagnose-Output zu liefern. Dafür gibt es panic!.

Die Trennung ist wichtig, weil sie unterschiedliche Werkzeuge erlaubt: Result-basierte Fehler werden behandelt, gemappt, propagiert; Panic-basierte werden gar nicht erst behandelt — sie sollen den Bug sichtbar machen. Wer beide Welten verwechselt, schreibt entweder fragilen Code (Panic, wo Result hingehört) oder unergonomischen Code (Result, wo Panic angemessen wäre).

Result als Wert

Das zentrale Konzept: ein Fehler ist ein Wert, der zurückgegeben wird — nicht etwas, das durch den Call-Stack geworfen wird.

Rust Result als Wert
use std::fs;

fn lese_config() -> Result<String, std::io::Error> {
    fs::read_to_string("/etc/myapp.conf")
}

fn main() {
    match lese_config() {
        Ok(inhalt) => println!("Config: {} Bytes", inhalt.len()),
        Err(e) => eprintln!("Fehler beim Lesen: {e}"),
    }
}

Im Beispiel siehst du die Grundmechanik. fs::read_to_string gibt Result<String, std::io::Error> zurück — entweder den Datei-Inhalt oder einen I/O-Fehler. Die lese_config-Funktion gibt den Result direkt weiter. Der Aufrufer im main muss mit einem match (oder einer anderen Methode) entscheiden, was bei jedem Fall passiert.

Das ist anders als in Exception-Sprachen: dort würde read_to_string entweder einen String zurückgeben oder eine Exception werfen, die vielleicht von einem catch-Block gefangen wird — vielleicht auch nicht. Bei Rust ist die Fehler-Behandlung im Typ-System verankert: solange du den Result nicht behandelst, hast du keinen Wert, mit dem du arbeiten kannst.

Der ?-Operator als Propagator

In der Praxis willst du Fehler oft nicht direkt behandeln, sondern an den Aufrufer weiterreichen — der entscheidet, was tun. Dafür gibt es den ?-Operator, der diesen häufigen Fall auf eine einzige Zeichen-Operation reduziert.

Rust ? für Propagation
use std::fs;
use std::io;

fn lese_und_zaehle() -> Result<usize, io::Error> {
    let inhalt = fs::read_to_string("/etc/myapp.conf")?;
    let zeilen = inhalt.lines().count();
    Ok(zeilen)
}

Der ? macht zwei Dinge: bei Ok(wert) extrahiert er den inneren wert und macht damit weiter; bei Err(e) wird return Err(e) ausgeführt — die Funktion bricht ab und der Aufrufer bekommt den Fehler. In einer Funktion mit drei oder vier potenziell scheiternden Schritten ist das ein dramatischer Lesbarkeits-Gewinn gegenüber expliziten match-Blöcken.

Mit zusätzlicher Macht: der ?-Operator führt automatisch eine From-Konvertierung des Error-Typs durch. Wenn deine Funktion Result<T, MeinFehler> zurückgibt und du einen ? auf ein Result<U, IoError> anwendest, sucht der Compiler nach einer From<IoError> for MeinFehler-Implementierung und nutzt sie. Damit kannst du verschiedene Stdlib-Fehler in deinen eigenen Error-Typ konvertieren, ohne überall explizit map_err zu schreiben.

Was dich erwartet

  • panic vs. recoverable — die Grenze zwischen Bugs (Panic) und erwarteten Fehlern (Result). Stack-Unwinding vs. Abort, panic!-Makro, panic = "abort" in Cargo.toml.
  • Result im Detail — vollständige API-Tour: map, and_then, map_err, unwrap, unwrap_or, unwrap_or_else, is_ok, is_err, Pattern-Matching-Idiome.
  • ?-Operator — die Mechanik im Detail, was er hinter den Kulissen macht, wo er funktioniert (Result, Option, ControlFlow), und die Stolperfallen.
  • From-Trait für Error-Conversion — wie der ?-Operator automatische Typ-Konvertierung nutzt, eigene From-Implementierungen, und das Pattern für Multi-Quell-Errors.
  • Eigene Error-Typen — von Hand geschriebene Error-Enums mit Display/Error-Impl, mit oder ohne source()-Chain, Best Practices.
  • thiserror-Pattern — das idiomatische Crate für Library-Errors. Boilerplate-Reduktion mit #[derive(thiserror::Error)], #[from]-Attribute, Display-Templates.
  • anyhow-Pattern — das idiomatische Crate für Application-Errors. anyhow::Result, Context mit .context(...), Error-Chains, der dynamische Error-Trait.
  • Error-Strategie — die wichtige Design-Frage: Library vs. Binary, wann thiserror, wann anyhow, wann beides, wann reine Stdlib reicht.

Was du nach diesem Kapitel kannst

  • Den Unterschied zwischen Bugs (panic) und erwarteten Fehlern (Result) erkennen und das richtige Konstrukt wählen.
  • Result<T, E> mit allen wichtigen Methoden idiomatisch verwenden — map, and_then, map_err, ?.
  • Eigene Error-Typen entwerfen, die sowohl gut auswertbar als auch gut darstellbar sind.
  • Den ?-Operator mit From-Conversion souverän einsetzen — auch über verschiedene Fehler-Typen hinweg.
  • thiserror für Library-Code und anyhow für Application-Code einsetzen — und die richtige Wahl zwischen beiden treffen.
  • Error-Chains nachvollziehen und debuggen (source()-Kette, Display-Output, Debug-Repräsentation).
  • Eine konsistente Error-Strategie für ein ganzes Projekt entwickeln.

Im nächsten Kapitel geht es zu Generics — Typ-Parameter und ihre Anwendung in Funktionen, Structs, Enums und Trait-Implementierungen.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Error Handling

Zur Übersicht