Der ?-Operator wäre nur halb so mächtig ohne die automatische Error-Conversion über den From-Trait. Wenn deine Funktion einen Result<T, MeinError> zurückgibt, der ?-Operator aber auf einem Result<U, StdError> angewendet wird, sucht der Compiler im Hintergrund nach einer From<StdError> for MeinError-Implementierung. Findet er sie, wird der Stdlib-Fehler automatisch in deinen Domain-Fehler konvertiert. Damit kannst du Multi-Quell-Error-Funktionen schreiben, in denen verschiedene Stdlib-Fehler (io::Error, ParseIntError, Utf8Error, ...) alle in einem einheitlichen Domain-Error-Enum landen — ohne manuellen Konvertierungs-Code an jeder Stelle.
Das Problem
Eine Funktion, die mit mehreren fallible Quell-Typen arbeitet, läuft in das Multi-Error-Problem: jede Quelle hat ihren eigenen Error-Typ.
use std::fs;
use std::num::ParseIntError;
use std::io;
// Welcher Error-Typ soll zurück?
// fn lese_zahl_v1(pfad: &str) -> Result<i32, ???> {
// let inhalt = fs::read_to_string(pfad)?; // io::Error
// let n: i32 = inhalt.trim().parse()?; // ParseIntError
// Ok(n)
// }fs::read_to_string gibt Result<String, io::Error> zurück. str::parse gibt Result<i32, ParseIntError> zurück. Die Funktion will beide nutzen — aber welcher Error-Typ soll als gemeinsame Rückgabe stehen?
Es gibt zwei klassische Lösungen: ein eigener Error-Enum mit Varianten pro Quell-Typ, oder ein Type-Erased Error wie Box<dyn Error>. Beide funktionieren mit From-Conversion zusammen, sodass der ?-Operator trotz Typ-Unterschieden funktioniert.
Eigener Error-Enum mit From
Die klassische Lösung: ein eigener Error-Enum mit From-Impls für jeden Quell-Typ.
use std::fs;
use std::num::ParseIntError;
use std::io;
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(ParseIntError),
}
impl From<io::Error> for AppError {
fn from(e: 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 n: i32 = inhalt.trim().parse()?; // ParseIntError → AppError::Parse
Ok(n)
}Drei Bausteine:
Der Error-Enum mit einer Variante pro Quell-Typ. AppError::Io(io::Error) wickelt einen I/O-Fehler ein, AppError::Parse(ParseIntError) einen Parse-Fehler. Beide tragen den Original-Fehler als Daten — die Information geht nicht verloren.
Die From-Implementierungen für jeden Quell-Typ. impl From<io::Error> for AppError definiert, wie ein io::Error in einen AppError umgewandelt wird. Die Implementierung ist meist trivial — einfach in die passende Variante wickeln.
Die Funktion mit Rückgabe Result<T, AppError>. Sie kann ? auf beliebigen Fehler-Typen anwenden, für die eine From-Impl zu AppError existiert. Der Compiler ruft automatisch From::from(quell_error) auf und produziert den passenden AppError.
Diese Variante ist die idiomatische Form für Library-Code: jeder Fehler-Typ ist explizit, kann unterschieden und einzeln behandelt werden. Konsumenten können per match auf die Varianten reagieren.
Das ?-Macro im Detail
Was passiert hinter den Kulissen, wenn der ?-Operator mit Typ-Konvertierung läuft?
# use std::fs;
# #[derive(Debug)] enum AppError { Io(std::io::Error) }
# impl From<std::io::Error> for AppError { fn from(e: std::io::Error) -> Self { AppError::Io(e) } }
// Original-Code:
fn lese() -> Result<String, AppError> {
let inhalt = fs::read_to_string("foo")?;
Ok(inhalt)
}
// Vom Compiler expandiert (vereinfacht):
fn lese_expandiert() -> Result<String, AppError> {
let inhalt = match fs::read_to_string("foo") {
Ok(s) => s,
Err(e) => return Err(From::from(e)), // <-- automatisches From::from
};
Ok(inhalt)
}Der Compiler übersetzt expr? intern in einen match-Block, in dem die Err-Variante per From::from konvertiert und dann zurückgegeben wird. Wenn die From-Impl nicht existiert, gibt es einen Compile-Fehler — der Compiler kann den Quell-Typ nicht in den Ziel-Typ überführen.
Wichtig: der Aufruf ist immer From::from, nicht Into::into. Das ist semantisch äquivalent (Blanket-Impl), aber syntaktisch unterscheidet es sich. Der Compiler sucht eine From<Quell> for Ziel-Impl.
From + Into Symmetrie
From und Into sind über eine Blanket-Implementation in der Stdlib verbunden:
// Aus der Stdlib (vereinfacht):
// impl<T, U: From<T>> Into<U> for T {
// fn into(self) -> U {
// U::from(self)
// }
// }Diese Blanket-Impl bedeutet: sobald du From<T> for U implementierst, bekommst du automatisch Into<U> for T dazu. Du musst Into niemals von Hand implementieren — sie ergibt sich.
In der Praxis: From::from(e) und e.into() sind beide äquivalent. Der ?-Operator nutzt intern From::from. Bei Funktions-Signaturen wie fn foo<E: Into<MeinError>>(...) profitierst du von der Blanket-Impl — alles, was From<_> for MeinError hat, kann übergeben werden.
Box<dyn Error> — Type-Erased Error
Eine Alternative zum eigenen Error-Enum: ein Type-Erased Error-Trait-Objekt.
use std::fs;
use std::error::Error;
fn lese_zahl(pfad: &str) -> Result<i32, Box<dyn Error>> {
let inhalt = fs::read_to_string(pfad)?; // io::Error → Box<dyn Error>
let n: i32 = inhalt.trim().parse()?; // ParseIntError → Box<dyn Error>
Ok(n)
}Box<dyn std::error::Error> ist ein Trait-Objekt, das jeden Typ akzeptiert, der den Error-Trait implementiert. Da fast alle Stdlib-Fehler-Typen Error implementieren, kannst du sie alle direkt mit ? propagieren — die Stdlib hat eine Blanket-Impl From<E: Error + 'static> for Box<dyn Error>, die die Konvertierung erledigt.
Vorteile: keine eigenen From-Impls nötig, sehr kompakt, ideal für Skripte und CLI-Programme. Nachteile: du verlierst Typ-Information — Konsumenten können den Fehler nicht mehr per Match unterscheiden, nur über downcast (was umständlich ist). Für Application-Code (Top-Level-Programme) ist das oft akzeptabel; für Library-Code (wo Konsumenten programmatisch reagieren wollen) ist der eigene Enum besser.
Box<dyn Error + Send + Sync>
use std::error::Error;
fn task() -> Result<(), Box<dyn Error + Send + Sync>> {
// Funktioniert auch über Thread-Grenzen
Ok(())
}In Multi-Threading-Kontexten brauchst du Box<dyn Error + Send + Sync> — der zusätzliche Trait-Bound macht den Error über Thread-Grenzen verschiebbar. Sehr typisch bei tokio-basierten async-Funktionen.
Verschachtelte From-Conversions
Die From-Mechanik ist transitiv anwendbar, aber nicht automatisch transitiv — du musst sie explizit verketten.
use std::num::ParseIntError;
#[derive(Debug)]
enum ParseError {
IntError(ParseIntError),
}
impl From<ParseIntError> for ParseError {
fn from(e: ParseIntError) -> Self { ParseError::IntError(e) }
}
#[derive(Debug)]
enum AppError {
Parse(ParseError),
}
impl From<ParseError> for AppError {
fn from(e: ParseError) -> Self { AppError::Parse(e) }
}
// Diese Funktion braucht ParseIntError → ParseError → AppError
// Aber direkt funktioniert es nicht!
// fn parse_app() -> Result<i32, AppError> {
// let n: i32 = "abc".parse()?; // Compile-Fehler — ParseIntError ≠ AppError
// }
// Lösung: zusätzliche From-Impl
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::Parse(ParseError::IntError(e))
}
}Der Compiler folgt keiner From-Kette: wenn From<A> for B und From<B> for C existieren, gibt es keinen automatischen From<A> for C. Du musst die direkte Impl explizit schreiben.
Das ist eine Designentscheidung — die Kompositions-Regel würde zu unklaren Konvertierungs-Pfaden führen und potenzielle Verhalt-Konflikte bringen. Die explizite Variante ist klarer.
Praxis: From-Conversion im echten Code
Domain-Error mit mehreren Quellen
use std::fs;
use std::io;
use std::num::ParseFloatError;
#[derive(Debug)]
pub enum DataError {
Io(io::Error),
Parse(ParseFloatError),
Validation(String),
}
impl From<io::Error> for DataError {
fn from(e: io::Error) -> Self { DataError::Io(e) }
}
impl From<ParseFloatError> for DataError {
fn from(e: ParseFloatError) -> Self { DataError::Parse(e) }
}
pub fn lese_messwert(pfad: &str) -> Result<f64, DataError> {
let raw = fs::read_to_string(pfad)?;
let wert: f64 = raw.trim().parse()?;
if wert < 0.0 {
return Err(DataError::Validation(format!("negativ: {wert}")));
}
Ok(wert)
}Drei Fehler-Quellen: I/O (io::Error), Parse (ParseFloatError), Domain-Validierung (custom). Erste zwei kommen via ? und automatischer From-Konvertierung; die dritte wird explizit per return Err produziert. Alle landen im gemeinsamen DataError-Enum.
Custom From mit Kontext
use std::io;
#[derive(Debug)]
pub enum AppError {
Config(String), // mit Kontext-String
Other(io::Error),
}
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self {
// Spezial-Behandlung für NotFound
if e.kind() == io::ErrorKind::NotFound {
AppError::Config(format!("Config-Datei fehlt: {e}"))
} else {
AppError::Other(e)
}
}
}From-Impls können beliebige Logik enthalten — nicht nur dumme Wrapping. Hier wird der io::Error inspiziert und je nach ErrorKind in unterschiedliche Varianten konvertiert. Achtung: diese „intelligente" From kann verwirren, wenn sie nicht offensichtlich aus der Impl klar ist. Pragmatisch besser ist oft explizites map_err an der Aufruf-Stelle.
Box<dyn Error> für ein Script
use std::error::Error;
use std::fs;
fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = std::env::args().collect();
let pfad = args.get(1).ok_or("Bitte Pfad angeben")?;
let inhalt = fs::read_to_string(pfad)?;
let n: i32 = inhalt.trim().parse()?;
println!("Wert: {n}");
Ok(())
}Box<dyn Error> als universeller Error-Typ. ? funktioniert auf allem, was Error implementiert — io::Error, ParseIntError, sogar dem &str (über From<&str>). Ideal für CLI-Scripts, wo keine programmatische Fehler-Differenzierung gebraucht wird.
Conversion mit map_err für ad-hoc-Fälle
use std::num::ParseIntError;
#[derive(Debug)]
pub enum CalcError {
ParseProblem { input: String, message: String },
DivisionDurchNull,
}
pub fn parse_und_teile(a: &str, b: &str) -> Result<i32, CalcError> {
let zahl_a: i32 = a.parse().map_err(|e: ParseIntError| {
CalcError::ParseProblem {
input: a.to_string(),
message: e.to_string(),
}
})?;
let zahl_b: i32 = b.parse().map_err(|e: ParseIntError| {
CalcError::ParseProblem {
input: b.to_string(),
message: e.to_string(),
}
})?;
if zahl_b == 0 {
return Err(CalcError::DivisionDurchNull);
}
Ok(zahl_a / zahl_b)
}map_err ist die Alternative zu From-Impls, wenn die Konvertierung Kontext braucht — etwa den Original-Input für eine bessere Fehler-Nachricht. Eine generische From<ParseIntError> könnte das nicht, weil sie keinen Zugriff auf den Input hat. map_err mit einer Closure schon.
Verschachtelter Error mit source-Chain
use std::error::Error;
use std::fmt;
use std::io;
#[derive(Debug)]
pub struct WrappedError {
kontext: String,
source: io::Error,
}
impl fmt::Display for WrappedError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}: {}", self.kontext, self.source)
}
}
impl Error for WrappedError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.source)
}
}
pub fn lade_mit_kontext(pfad: &str) -> Result<String, WrappedError> {
std::fs::read_to_string(pfad).map_err(|e| WrappedError {
kontext: format!("beim Laden von {pfad}"),
source: e,
})
}WrappedError ist ein eigener Error-Typ, der einen Stdlib-Fehler einwickelt und Kontext anreichert. Die source()-Methode im Error-Trait ermöglicht das Durchwandern der Fehler-Kette — Debugger und Logger können den ursprünglichen io::Error finden, auch wenn nur der Wrapped-Error gesehen wird.
Generischer Error mit Trait-Bound
use std::error::Error;
pub fn verarbeite<E: Error + 'static>(eingabe: Result<i32, E>) -> Result<i32, Box<dyn Error>> {
let n = eingabe?; // E → Box<dyn Error> via Blanket-Impl
Ok(n * 2)
}Generische Funktionen, die mit beliebigen Error-Typen umgehen können — durch die Error + 'static-Bounds und die Blanket-From<E: Error + 'static> for Box<dyn Error> funktioniert das transparent.
From in einem Trait
use std::error::Error;
pub trait Repository {
type Error: Error + Send + Sync + 'static;
fn finde_by_id(&self, id: u64) -> Result<Option<String>, Self::Error>;
}
pub fn lade_anzeige<R: Repository>(r: &R, id: u64) -> Result<String, Box<dyn Error + Send + Sync>>
where
R::Error: 'static,
{
let res = r.finde_by_id(id)?; // R::Error → Box<dyn Error + Send + Sync>
Ok(res.unwrap_or_else(|| "kein Eintrag".into()))
}Beim Trait-Design taucht oft ein associated Error type auf. Konsumenten der API können sich auf die Trait-Bounds verlassen, wodurch generisches Error-Handling möglich wird.
FAQ
? ruft From::from auf dem Error.
Die zentrale Mechanik. Bei expr? mit einem Err(e) wird return Err(From::from(e)) ausgeführt. Sucht eine From<QuellError> for ZielError-Impl und nutzt sie automatisch.
From implementiert Into automatisch.
Blanket-Impl in der Stdlib: impl<T, U: From<T>> Into<U> for T. Du brauchst Into niemals manuell zu implementieren. From::from(x) und x.into() sind äquivalent.
Eigener Error-Enum für Library, Box für CLI.
Bei Library-Code wollen Konsumenten programmatisch auf Fehler-Typen reagieren — eigener Enum mit klaren Varianten ist richtig. Bei CLI-Code reicht oft Box<dyn Error> als universelle Variante.
Keine automatische Transitivität bei From.
From<A> for B und From<B> for C ergeben nicht automatisch From<A> for C. Du musst die direkte Impl explizit schreiben. Designentscheidung gegen unklare Konvertierungs-Pfade.
map_err für ad-hoc oder Kontext-anreichernde Konvertierung.
Wenn keine From-Impl existiert oder Kontext nötig ist (Original-Input, Stelle im Code, etc.), nutze expr.map_err(|e| ...)?. Für wiederkehrende Konvertierungen lohnt sich eine echte From-Impl.
Box für Multi-Thread.
Wenn der Error über Thread-Grenzen muss (etwa in async/tokio), brauchst du die zusätzlichen Trait-Bounds. Sehr typisch für moderne Server-Code.
From<&str> for Box existiert.
Du kannst Err("nachricht")? direkt nutzen, wenn deine Funktion Result<_, Box<dyn Error>> zurückgibt. Sehr praktisch für schnelle Inline-Fehler in CLI-Tools.
Intelligente From-Impls sind oft Anti-Pattern.
Wenn deine From-Impl komplexe Logik enthält (Filtern, Klassifizieren, Kontext-Anreicherung), wird sie schwer nachvollziehbar. Pragmatisch besser: explizites map_err an der Aufruf-Stelle, das die Konvertierung sichtbar macht.
Weiterführende Ressourcen
Externe Quellen
- std::convert::From – Trait-Doc
- std::convert::Into – Trait-Doc
- std::error::Error – Trait-Doc
- The Rust Book – Defining Error Types
- Rust by Example – Wrapping errors