Result<T, E> ist Rusts Antwort auf das Exception-Problem. Statt ein Exception-Objekt durch den Call-Stack zu werfen, gibt jede fehlbare Funktion ein explizites Result<T, E> zurück: Ok(T) bei Erfolg, Err(E) bei Fehler. Der Aufrufer muss entscheiden, was zu tun ist — der Compiler lässt es nicht zu, dass ein Result einfach ignoriert wird. Mit dem ?-Operator wird Fehler-Propagation so kompakt wie ein modernes try/catch, ohne dass es die typische „unsichtbare Control-Flow"-Probleme von Exceptions hat. Dieser Artikel zeigt das vollständige API, die Patterns für Error-Conversion und den Übergang zu modernen Error-Crates wie thiserror und anyhow.

Definition

Result<T, E> ist ein generisches Stdlib-Enum:

Rust Stdlib-Definition
// Aus der Stdlib (vereinfacht):
pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

Wie Option sind beide Varianten im Prelude — du nutzt Ok(...) und Err(...) ohne Result::-Präfix.

Rust Verwendung
fn parse_alter(s: &str) -> Result<u32, String> {
    s.parse().map_err(|e: std::num::ParseIntError| format!("kein u32: {e}"))
}

fn main() {
    let a = parse_alter("28");
    let b = parse_alter("abc");
    println!("{a:?} {b:?}");
}

T ist der Erfolgs-Typ, E der Fehler-Typ. Beide können beliebig komplex sein.

must_use — Result darf nicht ignoriert werden

Result<T, E> ist mit #[must_use] markiert. Wer ein Result ohne Verarbeitung wegwirft, bekommt eine Compiler-Warnung:

Rust Warnung
fn schreibe(pfad: &str) -> Result<(), std::io::Error> {
    std::fs::write(pfad, "Hi")
}

fn main() {
    // schreibe("/tmp/foo");        // Warning: unused Result that must be used
    let _ = schreibe("/tmp/foo");   // explizit ignorieren (ok)
    // Oder:
    schreibe("/tmp/bar").unwrap();
}

Diese Warnung verhindert die häufigste Fehler-Quelle: „ich habe vergessen, das Ergebnis zu prüfen". In großen Codebases ist das Gold wert.

Der ?-Operator

Der wichtigste Operator für Result-Handling. Er macht aus einem Result<T, E> ein T (bei Ok) oder propagiert den Err als Funktions-Rückgabe:

Rust ?-Operator
use std::fs;

fn lese_und_zaehle(pfad: &str) -> Result<usize, std::io::Error> {
    let inhalt = fs::read_to_string(pfad)?;     // bei Err: return Err
    let zeilen = inhalt.lines().count();
    Ok(zeilen)
}

Ohne ?-Operator wäre das:

Rust Ohne ?
# use std::fs;
fn lese_und_zaehle(pfad: &str) -> Result<usize, std::io::Error> {
    let inhalt = match fs::read_to_string(pfad) {
        Ok(s) => s,
        Err(e) => return Err(e),
    };
    let zeilen = inhalt.lines().count();
    Ok(zeilen)
}

Verbose. ? macht das in einem Zeichen.

Mehrere Fehler-Typen mischen mit From

? kann unterschiedliche Error-Typen propagieren — solange eine From-Impl existiert:

Rust Mehrere Error-Typen
use std::fs;
use std::num::ParseIntError;

#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(ParseIntError),
}

impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}

impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}

fn lese_zahl(pfad: &str) -> Result<i32, AppError> {
    let inhalt = fs::read_to_string(pfad)?;     // io::Error → AppError::Io
    let zahl: i32 = inhalt.trim().parse()?;     // ParseIntError → AppError::Parse
    Ok(zahl)
}

? ruft automatisch From::from(...) auf dem Error-Typ auf. Damit kannst du verschiedene Stdlib-Fehler in deinen eigenen Error-Enum konvertieren — kompakte Fehler-Propagation über System-Grenzen.

Wichtige Methoden

unwrap, expect

Rust unwrap/expect
fn main() {
    let r: Result<i32, &str> = Ok(42);
    let n = r.unwrap();         // 42

    let e: Result<i32, &str> = Err("kaputt");
    // e.unwrap();              // Panic: "called Result::unwrap on Err: \"kaputt\""

    // expect mit Kontext:
    let m = e.expect("Lese-Operation darf nicht fehlschlagen");
    // Panic mit der Message
}

Wie bei Option: unwrap extrahiert oder panickt, expect mit Message.

unwrap_or, unwrap_or_else, unwrap_or_default

Rust Defaults
fn main() {
    let a: Result<i32, &str> = Err("fail");
    assert_eq!(a.unwrap_or(0), 0);
    assert_eq!(a.unwrap_or_else(|e| e.len() as i32), 4);

    let b: Result<String, &str> = Err("fail");
    assert_eq!(b.unwrap_or_default(), "");
}

map und map_err

Rust Transformation
fn main() {
    let a: Result<i32, &str> = Ok(5);
    let b = a.map(|n| n * 2);                   // Ok(10)
    assert_eq!(b, Ok(10));

    let c: Result<i32, &str> = Err("fail");
    let d = c.map_err(|e| format!("Fehler: {e}"));   // Err("Fehler: fail")
    assert_eq!(d, Err("Fehler: fail".to_string()));
}
  • map(f) — transformiert den Ok-Wert.
  • map_err(f) — transformiert den Err-Wert.

and_then

Rust and_then
fn parse_und_verdopple(s: &str) -> Result<i32, String> {
    s.parse::<i32>()
        .map_err(|e| e.to_string())
        .and_then(|n| {
            if n < 0 { Err("negative Zahl".into()) }
            else { Ok(n * 2) }
        })
}

fn main() {
    assert_eq!(parse_und_verdopple("5"), Ok(10));
    assert_eq!(parse_und_verdopple("-1"), Err("negative Zahl".into()));
    assert!(parse_und_verdopple("abc").is_err());
}

and_then chaint Result-Operationen. Sehr nützlich für Validierungs-Pipelines.

ok / err — zu Option konvertieren

Rust ok/err
fn main() {
    let r: Result<i32, &str> = Ok(42);
    let o: Option<i32> = r.ok();        // Some(42)

    let e: Result<i32, &str> = Err("fail");
    let e_opt: Option<&str> = e.err();   // Some("fail")
}

.ok() wirft den Fehler-Kontext weg. Nützlich, wenn dich der Fehler nicht interessiert, nur das Vorhandensein des Werts.

Eigene Error-Typen

In Library-Code definiert man typischerweise eigene Error-Enums:

Rust Manueller Error-Type
use std::fmt;

#[derive(Debug)]
pub enum ParseError {
    LeereEingabe,
    UnerwarteterTrenner(char),
    UngueltigeZahl(String),
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ParseError::LeereEingabe => write!(f, "Eingabe ist leer"),
            ParseError::UnerwarteterTrenner(c) =>
                write!(f, "Unerwarteter Trenner: '{c}'"),
            ParseError::UngueltigeZahl(s) =>
                write!(f, "'{s}' ist keine gültige Zahl"),
        }
    }
}

impl std::error::Error for ParseError {}

Drei Konventionen:

  • Enum mit Varianten pro Fehler-Art.
  • Debug-Derive für Entwickler-Output.
  • Display-Impl für End-User-Output.
  • std::error::Error-Impl — leerer Trait, der das Standard-Error-Interface erfüllt.

Mit thiserror-Crate

Das Boilerplate lässt sich mit dem thiserror-Crate eliminieren:

Rust thiserror
# // Erfordert Cargo.toml: thiserror = "1"
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ParseError {
    #[error("Eingabe ist leer")]
    LeereEingabe,

    #[error("Unerwarteter Trenner: '{0}'")]
    UnerwarteterTrenner(char),

    #[error("'{0}' ist keine gültige Zahl")]
    UngueltigeZahl(String),

    #[error("IO-Fehler beim Lesen")]
    Io(#[from] std::io::Error),
}

#[derive(Error)] generiert Display und std::error::Error. #[from] generiert eine From-Impl für automatische Konvertierung über den ?-Operator.

Box<dyn Error> und anyhow

Für Anwendungs-Code (im Gegensatz zu Library-Code) ist oft ein generischer Error-Typ einfacher:

Rust Box dyn Error
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let inhalt = std::fs::read_to_string("config.txt")?;
    let zahl: i32 = inhalt.trim().parse()?;
    println!("{zahl}");
    Ok(())
}

Box<dyn std::error::Error> ist ein „Anywhere-Error" — er nimmt jeden Typ auf, der std::error::Error implementiert. Sehr praktisch in main und Application-Code.

Noch eleganter mit dem anyhow-Crate:

Rust anyhow
# // Erfordert Cargo.toml: anyhow = "1"
use anyhow::{Result, Context};

fn main() -> Result<()> {
    let inhalt = std::fs::read_to_string("config.txt")
        .context("Lese config.txt")?;
    let zahl: i32 = inhalt.trim().parse()
        .context("Parse Inhalt als Zahl")?;
    println!("{zahl}");
    Ok(())
}

anyhow::Result<T> ist Result<T, anyhow::Error> — ein flexibler Error-Typ. .context(...) hängt Kontext-Info an den Fehler. Bei einem Crash sieht man den ganzen „Pfad", nicht nur den letzten Fehler.

Library vs. Application

Code-TypEmpfohlene Error-Strategie
LibraryEigenes Error-Enum + thiserror
Application / CLIanyhow::Result mit .context(...)
Prototyp / SkriptBox<dyn Error> oder anyhow

Praxis: Result im echten Code

Datei lesen mit ?

Rust Lese-Funktion
use std::fs;
use std::io;

pub fn lese_zeilen(pfad: &str) -> io::Result<Vec<String>> {
    let inhalt = fs::read_to_string(pfad)?;
    Ok(inhalt.lines().map(String::from).collect())
}

fn main() -> io::Result<()> {
    let zeilen = lese_zeilen("/etc/hosts")?;
    for z in zeilen.iter().take(5) {
        println!("{z}");
    }
    Ok(())
}

io::Result<T> ist Result<T, io::Error>. Sehr verbreitete Konvention.

Validierungs-Pipeline

Rust Email-Validator
#[derive(Debug)]
pub enum ValidationError {
    Leer,
    KeinAt,
    KeinPunkt,
    ZuLang(usize),
}

pub fn validiere_email(s: &str) -> Result<&str, ValidationError> {
    let s = s.trim();
    if s.is_empty() { return Err(ValidationError::Leer); }
    if s.len() > 254 { return Err(ValidationError::ZuLang(s.len())); }
    if !s.contains('@') { return Err(ValidationError::KeinAt); }
    let teile: Vec<&str> = s.split('@').collect();
    if teile.len() != 2 { return Err(ValidationError::KeinAt); }
    if !teile[1].contains('.') { return Err(ValidationError::KeinPunkt); }
    Ok(s)
}

Mehrere Validierungs-Checks mit Early-Returns. Klare Fehler-Diagnose pro Fall.

Error-Konversion via From

Rust Multi-Error-Function
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ConfigError {
    #[error("IO-Fehler: {0}")]
    Io(#[from] std::io::Error),
    #[error("Parse-Fehler in Zeile {zeile}: {grund}")]
    Parse { zeile: u32, grund: String },
}

pub fn lade_config(pfad: &str) -> Result<u32, ConfigError> {
    let inhalt = std::fs::read_to_string(pfad)?;        // io::Error → ConfigError::Io
    for (i, zeile) in inhalt.lines().enumerate() {
        if let Some(wert) = zeile.strip_prefix("port=") {
            return wert.parse().map_err(|_| ConfigError::Parse {
                zeile: i as u32 + 1,
                grund: format!("'{wert}' ist keine Zahl"),
            });
        }
    }
    Err(ConfigError::Parse { zeile: 0, grund: "port nicht gefunden".into() })
}

#[from] macht ? mit io::Error direkt möglich. Manuell mit map_err für komplexere Konvertierungen.

Iteration mit collect für Result

Rust Parse-Pipeline
pub fn parse_alle(strings: &[&str]) -> Result<Vec<i32>, String> {
    strings.iter()
        .map(|s| s.parse::<i32>().map_err(|e| format!("'{s}': {e}")))
        .collect()       // collect von Iterator<Result<T, E>> zu Result<Vec<T>, E>
}

fn main() {
    assert_eq!(parse_alle(&["1", "2", "3"]), Ok(vec![1, 2, 3]));
    assert!(parse_alle(&["1", "abc", "3"]).is_err());
}

Sehr elegant: collect auf Iterator<Result<T, E>> ergibt Result<Vec<T>, E> — bei erstem Err bricht es ab.

Result als Funktions-Rückgabe in main

Rust main mit Result
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args: Vec<String> = std::env::args().collect();
    let pfad = args.get(1).ok_or("Aufruf: programm <pfad>")?;
    let inhalt = std::fs::read_to_string(pfad)?;
    println!("{inhalt}");
    Ok(())
}

Seit Rust 1.26 darf main Result zurückgeben. Bei Err wird ein non-zero Exit-Code gesetzt und der Error gedruckt.

Conversion: Option zu Result

Rust ok_or
pub fn finde_konfig(env_var: &str) -> Result<String, &'static str> {
    std::env::var(env_var).ok().ok_or("Env-Variable nicht gesetzt")
}

Option::ok_or(err) macht aus Option<T> ein Result<T, E> — sehr typisches Pattern.

Result in match

Rust Match auf Result
fn main() {
    match std::fs::read_to_string("config.txt") {
        Ok(inhalt) => println!("{} Bytes", inhalt.len()),
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            println!("Datei nicht da — nutze Default");
        }
        Err(e) => {
            eprintln!("Fehler: {e}");
        }
    }
}

Match mit Guards für Fehler-Spezifikation. e.kind() gibt die ErrorKind-Enum-Variante.

Mit anyhow für Application-Code

Rust anyhow-Workflow
# // Erfordert Cargo.toml: anyhow = "1"
use anyhow::{Context, Result};

fn lade_und_parse(pfad: &str) -> Result<i32> {
    let inhalt = std::fs::read_to_string(pfad)
        .with_context(|| format!("Lese Datei {pfad}"))?;
    let zahl: i32 = inhalt.trim().parse()
        .with_context(|| format!("Parse '{}' als Zahl", inhalt.trim()))?;
    Ok(zahl)
}

with_context hängt einen lazy berechneten Kontext-String an. Bei Fehler zeigt anyhow die ganze Kette.

Retry-Logik mit Result

Rust Retry
pub fn versuche_mit_retry<T, E, F>(mut f: F, max_versuche: u32) -> Result<T, E>
where
    F: FnMut() -> Result<T, E>,
{
    let mut letzter_fehler = None;
    for _ in 0..max_versuche {
        match f() {
            Ok(v) => return Ok(v),
            Err(e) => letzter_fehler = Some(e),
        }
    }
    Err(letzter_fehler.unwrap())
}

Generische Retry-Funktion. Closure liefert Result, wird mehrfach versucht.

Interessantes

Result ist must_use.

Wer ein Result wegwirft, bekommt Compiler-Warning. Das verhindert die häufigste Fehler-Quelle: vergessene Error-Checks. Explizit ignorieren: let _ = funktion(); oder funktion().unwrap().

? ruft From::from(err) automatisch auf.

Wenn deine Funktion Result<T, E1> zurückgibt und du funktion()? auf einer Result<U, E2> aufrufst, konvertiert ? automatisch E2 → E1 via From-Impl. Damit kannst du verschiedene Stdlib-Fehler in deinem Domain-Error sammeln.

map wirkt auf Ok, map_err auf Err.

Symmetrisch zu den beiden Varianten. result.map(|t| ...).map_err(|e| ...) ist eine Kette von zwei unabhängigen Transformationen.

collect auf Iterator ist mächtig.

iter.map(|x| try_something(x)).collect::<Result<Vec<_>, _>>() macht aus einer Reihe von Versuchen entweder einen vollen Vec (alle erfolgreich) oder den ersten Err. Sehr typische Validierungs-Pipeline.

Eigene Error-Enums mit thiserror sind Standard.

Manuelles Display + Error-Trait ist verbose. #[derive(Error)] mit #[error("...")]-Attributen pro Variante macht das in einer Zeile pro Fehler-Fall. #[from] für automatische From-Impls.

anyhow für Application-Code, thiserror für Library.

anyhow::Result<T> ist ein flexibler Catch-All — perfekt in main und CLIs. thiserror ist strukturierter und exponiert konkrete Fehler-Varianten — besser für Libraries, die Konsumenten erlauben, auf Fehler-Arten zu reagieren.

? funktioniert auch auf Option.

In Funktionen, die Option<T> zurückgeben, propagiert ? ein None. In Funktionen mit Result<T, E>, propagiert ? ein Err. Beide Typen sind im Prelude und der ?-Operator wählt automatisch.

fn main() -> Result ist legal seit Rust 1.26.

Damit lassen sich Fehler in main mit ? propagieren. Bei Err wird ein non-zero Exit-Code gesetzt und der Fehler gedruckt. Sehr kompakt für CLIs.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Enums & Pattern Matching

Zur Übersicht