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.

Rust Verschiedene Quellen, verschiedene Errors
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.

Rust Eigener Error mit From
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?

Rust Wie ? expandiert
# 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:

Rust From/Into Blanket-Impl
// 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.

Rust Box dyn Error
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>

Rust Mit Thread-Bounds
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.

Rust Mehrere Stufen
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

Rust Multi-Source-Error
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

Rust From mit Anreicherung
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

Rust CLI mit Box dyn Error
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

Rust map_err
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

Rust Error mit source
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

Rust Generic Error
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

Rust Trait mit From-Bound
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

/ Weiter

Zurück zu Error Handling

Zur Übersicht