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:
// 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:
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
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
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
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
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
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
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
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
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
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.
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
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.
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
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
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
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"
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
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
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
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
- std::result – Modul-Doc
- Result-Methoden – API-Referenz
- The Rust Book – Recoverable Errors with Result
- Rust by Example – Result