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:
// 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.
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:
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:
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:
# 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:
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
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
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
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 denOk-Wert.map_err(f)— transformiert denErr-Wert.
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
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:
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:
# // 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:
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:
# // 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-Typ | Empfohlene Error-Strategie |
|---|---|
| Library | Eigenes Error-Enum + thiserror |
| Application / CLI | anyhow::Result mit .context(...) |
| Prototyp / Skript | Box<dyn Error> oder anyhow |
Praxis: Result im echten Code
Datei lesen mit ?
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
#[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
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
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
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
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
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
# // 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
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
- The Rust Book – Recoverable Errors with Result
- std::result
- std::result::Result Methoden
- thiserror-Crate
- anyhow-Crate
- Error Handling Project Group