Der Result-Typ wurde im Enums-Kapitel kurz vorgestellt — dieser Artikel ist die vollständige API-Tour. Result<T, E> hat eine reichhaltige Methoden-Sammlung, die viel mehr ermöglicht als nur match und unwrap. Mit den Kombinatoren map, and_then, map_err, or_else baust du elegante Pipelines aus fehlbaren Operationen. Mit unwrap_or und seinen Verwandten holst du Werte heraus, ohne Panic-Risiko. Mit is_ok/is_err-Helfern und Konvertierungen zwischen Result und Option deckst du alle defensiven Programmierungs-Patterns ab. Wer das ganze API kennt, schreibt deutlich kompakteren und ausdrucksstärkeren Code.

Result als Sum-Type

Bevor wir zum API gehen, eine kurze Erinnerung an die Struktur:

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

Result<T, E> ist ein Sum-Type mit zwei Varianten. Ok(T) enthält den Erfolgs-Wert (vom Typ T), Err(E) enthält den Fehler-Wert (vom Typ E). Beide Varianten sind im Prelude verfügbar — du nutzt Ok(...) und Err(...) ohne Result::-Präfix.

Wichtig zu wissen: Result<T, E> ist mit #[must_use] markiert. Wer das Ergebnis einer fallible Funktion ohne Behandlung wegwirft, bekommt eine Compiler-Warnung. Das ist eines der zentralen Sicherheits-Features — es verhindert die häufigste Fehler-Quelle in C-artigen Sprachen: „ich habe vergessen, den Return-Code zu prüfen".

Pattern-Matching — die Grundform

Der direkteste Weg, mit einem Result umzugehen, ist match:

Rust match auf Result
fn parse_zahl(s: &str) -> Result<i32, std::num::ParseIntError> {
    s.parse()
}

fn main() {
    let r = parse_zahl("42");
    match r {
        Ok(n) => println!("Wert: {n}"),
        Err(e) => eprintln!("Fehler: {e}"),
    }
}

Beide Arme sind Pflicht — der Compiler verlangt Exhaustiveness. Das ist der Default-Stil für Code, der lokal auf den Fehler reagieren muss (Logging, Default-Wert setzen, Retry-Logik). Für reine Propagation gibt's elegantere Formen (siehe ?-Operator-Artikel), aber match ist die Grundlage für alles weitere.

if let — wenn dich nur ein Fall interessiert

Rust if let
fn drucke_wenn_erfolg(r: Result<i32, &str>) {
    if let Ok(n) = r {
        println!("Erfolg: {n}");
    }
    // Err wird ignoriert
}

Wenn nur der Ok-Fall relevant ist, ersetzt if let einen vollen match mit Err(_) => {}-Arm. Genauso umgekehrt mit if let Err(e) = r.

Extraktion: unwrap-Familie

Die einfachste Form, an den inneren Wert zu kommen, ist die unwrap-Familie. Sie liefert den Wert oder panickt — oder gibt einen Default zurück.

unwrap und expect

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

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

    // expect mit eigener Nachricht:
    // e.expect("Wert sollte da sein");  // Panic mit der Message
}

unwrap ist die einfachste Variante: bei Ok(x) gibt sie x zurück, bei Err(e) panickt sie mit der Standard-Nachricht called Result::unwrap on Err: .... Sie ist semantisch eine Aussage: „ich erwarte hier kein Fehler — wenn doch, ist es ein Bug".

expect("nachricht") ist die idiomatischere Variante: sie panickt mit deiner eigenen Nachricht statt der Standard-Form. In Production-Code solltest du fast immer expect statt unwrap verwenden — die selbst formulierte Nachricht macht das Debugging dramatisch einfacher. Die Konvention: erkläre, warum der Fehler nicht auftreten sollte, nicht was schiefging.

Beide sollten nur in drei Situationen verwendet werden: Tests (Panic ist akzeptables Test-Verhalten), Prototypen/Skripte (Schnelligkeit zählt), oder Code, wo der Fehler beweisbar unmöglich ist (etwa vec.first().unwrap() direkt nach len() > 0-Check).

unwrap_or — Default-Wert

Rust unwrap_or
fn main() {
    let a: Result<i32, &str> = Ok(42);
    let b: Result<i32, &str> = Err("fail");
    assert_eq!(a.unwrap_or(0), 42);
    assert_eq!(b.unwrap_or(0), 0);
}

unwrap_or(default) ist die sichere Variante: bei Ok(x) gibt sie x zurück, bei Err(_) den übergebenen Default. Kein Panic, kein Pattern-Match. Wichtig zu wissen: der Default-Wert wird immer ausgewertet, auch wenn er nicht verwendet wird — bei teuren Defaults siehe unwrap_or_else.

unwrap_or_else — lazy Default

Rust unwrap_or_else
fn main() {
    let r: Result<String, &str> = Err("DB-Fehler");
    let s = r.unwrap_or_else(|e| {
        eprintln!("Warnung: {e}");
        String::from("default")
    });
    println!("{s}");
}

unwrap_or_else(closure) ist die lazy Variante: die Closure wird nur bei Err aufgerufen. Sie bekommt den Error-Wert als Parameter und kann mit ihm arbeiten — Logging, Default-Berechnung basierend auf dem Fehler-Typ, oder schlicht ein konstanter Wert.

Bei billigen Defaults (Konstanten) reicht unwrap_or. Bei teuren Defaults (Allocations, Berechnungen) ist unwrap_or_else korrekt — sonst läuft die teure Operation bei jedem Aufruf, auch wenn das Result Ok ist.

unwrap_or_default — via Default-Trait

Rust unwrap_or_default
fn main() {
    let r1: Result<String, &str> = Err("fail");
    assert_eq!(r1.unwrap_or_default(), "");      // String::default() = ""

    let r2: Result<i32, &str> = Err("fail");
    assert_eq!(r2.unwrap_or_default(), 0);       // i32::default() = 0
}

unwrap_or_default() nutzt den Default::default()-Wert des Erfolgs-Typs als Fallback. Funktioniert für alle Typen, die Default implementieren — String, Vec, i32, bool, eigene Typen mit Derive-Default oder eigener Impl.

Transformationen: map und Co.

Die Kombinatoren sind das eigentliche Highlight der Result-API. Sie erlauben fehlbare Pipelines ohne explizites Pattern-Matching auf jeder Stufe.

map — Erfolgs-Wert transformieren

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

    let e: Result<i32, &str> = Err("fail");
    let nichts = e.map(|n| n * 2);
    assert_eq!(nichts, Err("fail"));        // Err propagiert unverändert
}

map(closure) transformiert den inneren Erfolgs-Wert, falls vorhanden. Bei Ok(x) wird die Closure auf x angewendet und das Ergebnis als Ok(y) zurückgegeben. Bei Err(e) läuft die Closure nicht — der Error wird unverändert durchgereicht.

Damit kannst du Transformations-Ketten bauen: result.map(f).map(g).map(h) läuft bei Ok alle Schritte durch, bei Err springt sofort zum Ende. Sehr ähnlich zur Option::map (siehe Option-Artikel).

map_err — Fehler-Wert transformieren

Rust map_err
fn main() {
    let r: Result<i32, std::num::ParseIntError> = "abc".parse();
    let mit_kontext: Result<i32, String> = r.map_err(|e| {
        format!("Parse-Fehler: {e}")
    });
    assert!(mit_kontext.is_err());
    // Err("Parse-Fehler: invalid digit found in string")
}

map_err(closure) ist das Gegenstück zu map: es transformiert den Fehler-Wert, lässt den Erfolgs-Wert unverändert. Sehr nützlich an API-Grenzen, wo du einen Stdlib-Fehler in deinen eigenen Fehler-Typ umwandeln willst — etwa von ParseIntError zu einem String mit Kontext, oder zu einem eigenen Error-Enum.

Mit map_err plus ?-Operator wird die Fehler-Konvertierung sehr kompakt: value.parse().map_err(|e| MeinError::Parse(e))? parst, mappt den Fehler, und propagiert ihn — alles in einer Zeile.

and_then — fehlbare Folge-Operation

Rust and_then
fn parse_und_quadriere(s: &str) -> Result<i32, std::num::ParseIntError> {
    s.parse::<i32>().and_then(|n| {
        if n < 0 {
            "negativ".parse::<i32>()       // erzwingt einen ParseIntError
        } else {
            Ok(n * n)
        }
    })
}

fn main() {
    assert_eq!(parse_und_quadriere("5"), Ok(25));
    assert!(parse_und_quadriere("abc").is_err());
    assert!(parse_und_quadriere("-5").is_err());
}

and_then(closure) ist die mächtige Variante von map: die Closure gibt selbst ein Result zurück, statt nur einen Wert. Damit kannst du fehlbare Operationen verketten — jede kann scheitern, und der Fehler propagiert durch die Kette.

In funktionalen Sprachen ist diese Operation als bind oder flatMap bekannt — sie ist die Monad-Operation für die Result-Monade. In der Praxis brauchst du sie selten direkt, weil der ?-Operator das gleiche Pattern eleganter ausdrückt. Aber für funktional-orientierten Code oder spezifische Pipeline-Patterns ist and_then das Werkzeug.

or_else — Fallback bei Fehler

Rust or_else
fn main() {
    let primary: Result<String, &str> = Err("primary down");
    let r = primary.or_else(|_e| -> Result<String, &str> {
        Ok(String::from("fallback"))
    });
    assert_eq!(r, Ok(String::from("fallback")));
}

or_else(closure) ist das Gegenstück zu and_then: bei Ok wird der Wert unverändert weitergegeben; bei Err läuft die Closure und liefert ein neues Result. Damit kannst du Fallback-Logik bauen — etwa „versuche Server A, wenn das scheitert versuche Server B".

Konvertierungen zu Option

Manchmal willst du aus einem Result eine Option machen — etwa wenn der Fehler-Wert nicht interessiert.

Rust ok / err
fn main() {
    let r1: Result<i32, &str> = Ok(42);
    let r2: Result<i32, &str> = Err("fail");

    // ok(): Result<T, E> → Option<T>
    assert_eq!(r1.ok(), Some(42));
    assert_eq!(r2.ok(), None);

    // err(): Result<T, E> → Option<E>
    assert_eq!(r1.err(), None);
    assert_eq!(r2.err(), Some("fail"));
}

ok() macht aus Ok(x) ein Some(x) und aus Err(_) ein None — der Fehler wird verworfen. Sehr nützlich in Iterator-Pipelines mit filter_map: iter.filter_map(|x| x.parse().ok()) collect't nur die parsbaren Werte.

err() ist das Spiegelbild: Ok(_) wird zu None, Err(e) zu Some(e). Selten gebraucht, aber für Diagnose-Code nützlich („sammle alle Fehler in einer Liste").

Inspektion und Tests

Rust is_ok / is_err
fn main() {
    let r1: Result<i32, &str> = Ok(42);
    let r2: Result<i32, &str> = Err("fail");

    assert!(r1.is_ok());
    assert!(!r1.is_err());
    assert!(r2.is_err());

    // is_ok_and / is_err_and mit Bedingung
    assert!(r1.is_ok_and(|n| n > 10));
    assert!(!r1.is_ok_and(|n| n > 100));
    assert!(r2.is_err_and(|e| e.starts_with("f")));
}

is_ok() und is_err() sind die boolschen Test-Methoden. Sie sind selten der idiomatische Weg — meist willst du auch den Wert verwenden, dann ist Pattern-Matching oder ein Kombinator besser. Aber für reine Existenz-Checks (Logging, Statistik) sind sie das richtige Werkzeug.

is_ok_and(closure) und is_err_and(closure) (seit Rust 1.70) kombinieren den Test mit einer zusätzlichen Bedingung — etwa „ist Ok UND der Wert größer als 10". Spart einen verschachtelten Match.

Sammeln in Iterator-Pipelines

Eine besonders elegante Eigenschaft: Result implementiert FromIterator so, dass collect::<Result<Vec<T>, E>>() bei Fehler early-return macht.

Rust collect zu Result
fn main() {
    let alle_ok = ["1", "2", "3"];
    let r1: Result<Vec<i32>, _> = alle_ok.iter().map(|s| s.parse()).collect();
    assert_eq!(r1, Ok(vec![1, 2, 3]));

    let mit_fehler = ["1", "abc", "3"];
    let r2: Result<Vec<i32>, _> = mit_fehler.iter().map(|s| s.parse()).collect();
    assert!(r2.is_err());           // gesamtes Ergebnis ist Err
}

collect::<Result<Vec<T>, E>>() ist eine sehr nützliche Spezialisierung. Statt einen Vec<Result<T, E>> zu produzieren (jedes Element für sich), produziert sie ein einziges Result: bei allen Ok ein Ok(vec), bei mindestens einem Err ein Err(e) mit dem ersten Fehler.

Das funktioniert über die FromIterator<Result<T, E>>-Implementation für Result<Vec<T>, E>. Im Hintergrund läuft die Iteration und stoppt beim ersten Fehler — early-return mit dem Fehler als Resultat.

Praxis: Result-Patterns im echten Code

File-Read mit Default

Rust Default bei Fehler
use std::fs;

pub fn lese_config_oder_default(pfad: &str) -> String {
    fs::read_to_string(pfad).unwrap_or_else(|_| {
        String::from("# Default-Config\nhost=localhost")
    })
}

Klassisches Pattern: versuche die Konfig zu laden, fall zurück auf einen Default. unwrap_or_else mit Closure, weil der Default-String allokiert wird — bei unwrap_or würde er bei jedem Aufruf erzeugt, auch wenn die Datei lesbar war.

Parse-Pipeline mit map_err

Rust Multi-Step-Parse
pub fn parse_alter(s: &str) -> Result<u32, String> {
    s.trim()
        .parse::<u32>()
        .map_err(|e| format!("Alter '{s}' ungültig: {e}"))
        .and_then(|n| {
            if n > 150 {
                Err(format!("Alter {n} unrealistisch"))
            } else {
                Ok(n)
            }
        })
}

Drei Schritte: parse (kann scheitern), Error-Kontext anreichern, Domain-Validierung mit weiterer Fehler-Möglichkeit. Alles in einer Chain, ohne explizite if/else-Verschachtelung.

filter_map mit ok

Rust Iterator-Filter
fn main() {
    let inputs = ["1", "abc", "3", "x", "5"];
    let valid: Vec<i32> = inputs.iter()
        .filter_map(|s| s.parse::<i32>().ok())
        .collect();
    assert_eq!(valid, vec![1, 3, 5]);
}

Sehr typisches Pattern für „extrahiere alle parsbaren Werte, ignoriere die anderen". parse().ok() macht aus Result ein Option, filter_map collect't nur die Some-Werte.

collect mit Result für „alle oder keiner"

Rust All-or-Nothing-Parsing
pub fn parse_alle_zahlen(inputs: &[&str]) -> Result<Vec<i32>, std::num::ParseIntError> {
    inputs.iter().map(|s| s.parse()).collect()
}

fn main() {
    let alle_ok = ["1", "2", "3"];
    assert!(parse_alle_zahlen(&alle_ok).is_ok());

    let mit_fehler = ["1", "abc", "3"];
    assert!(parse_alle_zahlen(&mit_fehler).is_err());
}

Das Gegenstück zum filter_map-Pattern: hier soll jeder Input parsbar sein, sonst ist die ganze Operation gescheitert. collect::<Result<Vec<i32>, _>> macht das atomar — entweder ein Vec mit allen Zahlen, oder der erste Fehler.

Fallback-Chain mit or_else

Rust Multi-Source-Lookup
use std::fs;
use std::env;

pub fn lese_konfig() -> Result<String, String> {
    fs::read_to_string("/etc/myapp.conf")
        .or_else(|_| fs::read_to_string("./myapp.conf"))
        .or_else(|_| {
            env::var("MYAPP_CONFIG")
                .map_err(|e| format!("Keine Config gefunden: {e}"))
        })
}

Suche an mehreren Stellen, die erste erfolgreiche Quelle gewinnt. or_else mit jeweils einem Fallback. Wenn alle scheitern, kommt der letzte Fehler — hier mit map_err in eine sprechende Nachricht umgewandelt.

Result als Funktions-Signatur-Hilfe

Rust Domain-Logic mit Result
pub struct User { age: u32 }

#[derive(Debug)]
pub enum ValidationError {
    TooYoung(u32),
    TooOld(u32),
}

impl User {
    pub fn validiere(&self) -> Result<(), ValidationError> {
        if self.age < 18 {
            return Err(ValidationError::TooYoung(self.age));
        }
        if self.age > 120 {
            return Err(ValidationError::TooOld(self.age));
        }
        Ok(())
    }
}

Result<(), Error> als Rückgabe ist das idiomatische Pattern für „Validierungs-Funktion ohne Daten-Rückgabe". Der Aufrufer prüft mit ? oder Pattern-Match, ob die Validierung erfolgreich war.

Match mit Pattern-Bindings

Rust Detaillierte Behandlung
use std::io;

pub fn behandle_io(r: Result<String, io::Error>) {
    match r {
        Ok(text) => println!("Lese erfolgreich: {} Zeichen", text.len()),
        Err(e) if e.kind() == io::ErrorKind::NotFound => {
            eprintln!("Datei fehlt — wird neu angelegt");
        }
        Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
            eprintln!("Zugriff verweigert: sudo benötigt");
        }
        Err(e) => eprintln!("Anderer I/O-Fehler: {e}"),
    }
}

Match mit Guards für differenzierte Fehler-Behandlung. Verschiedene io::ErrorKind-Varianten bekommen unterschiedliche Reaktionen. Der Compiler erzwingt Exhaustiveness — wenn du eine wichtige Variante vergisst, gibt es eine Warnung.

FAQ

Wann unwrap, wann expect?

Beide panicken bei Err. unwrap liefert die Standard-Panic-Message, expect("...") deine eigene. In Production-Code praktisch immer expect — die selbst formulierte Begründung hilft beim Debugging enorm.

unwrap_or vs. unwrap_or_else: lazy or not?

unwrap_or(default) wertet default immer aus — auch bei Ok. unwrap_or_else(|| default()) nur bei Err. Bei billigen Defaults (Konstanten) ist unwrap_or ok, bei teuren (Allocations) ist unwrap_or_else korrekt.

map transformiert Ok, map_err transformiert Err.

Symmetrische Operationen. map arbeitet auf dem Erfolgs-Wert (Err bleibt unangetastet), map_err auf dem Fehler-Wert (Ok bleibt unangetastet). Beide produzieren wieder ein Result.

and_then ist map, das selbst ein Result liefert.

Wenn die Transformations-Closure auch scheitern kann, ist and_then das richtige Werkzeug. In funktionalen Sprachen heißt das Bind/flatMap. In Rust ist es oft durch den ?-Operator ersetzbar — aber für Method-Chain-Stile bleibt es nützlich.

collect::>() macht Early-Return bei Fehler.

Sehr eleganter Spezialfall: aus einem Iterator von Results wird ein einzelnes Result. Bei allen Ok der gesammelte Vec, beim ersten Err der Abbruch mit diesem Fehler. Klassisches „all-or-nothing"-Pattern.

ok() und err() konvertieren zu Option.

r.ok() macht Ok(x) zu Some(x), Err zu None. r.err() umgekehrt. Sehr nützlich in Iterator-Pipelines mit filter_map oder wenn der Fehler verworfen werden soll.

is_ok/is_err nur für reine Existenz-Checks.

Wenn du den Wert auch brauchst, ist Pattern-Match oder Kombinator besser. Boolean-Tests sind für Statistik, Logging, Verzweigungs-Logik ohne Wert-Verwendung.

Result ist #[must_use].

Compile-Warnung, wenn du das Ergebnis einer fallible Funktion ignorierst. Bewusste Unterdrückung mit let _ = .... Diese Mechanik verhindert die häufigste C-Bug-Quelle.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Error Handling

Zur Übersicht