Der ?-Operator ist eines der am häufigsten verwendeten Sprach-Features in Rust. Was vor seiner Einführung mehrere Zeilen mit match-Blöcken brauchte, ist heute ein einziges Zeichen am Ende eines Ausdrucks. Der Operator macht aus einem Result<T, E> ein T (im Erfolgsfall) oder propagiert den Err als Funktions-Rückgabe (im Fehlerfall). Dazu kommt automatische From-Konvertierung des Error-Typs — du kannst verschiedene Stdlib-Fehler in deinen eigenen Error-Typ überführen, ohne dafür explizit Code schreiben zu müssen. Dieser Artikel zeigt die Mechanik im Detail, die Voraussetzungen, die Sonderformen für Option und ControlFlow, sowie die typischen Stolperfallen.

Was der ?-Operator macht

Die Mechanik ist konzeptuell einfach. Auf einem Result<T, E>:

Rust Mit und ohne ?
use std::fs;

// Mit ?
fn lese_und_zaehle(pfad: &str) -> Result<usize, std::io::Error> {
    let inhalt = fs::read_to_string(pfad)?;
    Ok(inhalt.lines().count())
}

// Ohne ? — explizit
fn lese_und_zaehle_explizit(pfad: &str) -> Result<usize, std::io::Error> {
    let inhalt = match fs::read_to_string(pfad) {
        Ok(s) => s,
        Err(e) => return Err(e),
    };
    Ok(inhalt.lines().count())
}

Beide Funktionen tun semantisch das gleiche. Die ?-Version ist eine Zeile, die explizite ist vier. Der Compiler übersetzt den ? intern in genau diesen match-Block.

Konkret macht expr? bei einem Result: wenn expr Ok(wert) ist, wird wert extrahiert und als Wert des ?-Ausdrucks verwendet — der Code läuft normal weiter. Wenn expr Err(e) ist, wird return Err(From::from(e)) ausgeführt — die Funktion bricht ab, der Aufrufer bekommt den Fehler.

Bei mehreren ?-Operationen in einer Funktion summiert sich der Effekt: aus zwanzig Zeilen Match-Boilerplate werden fünf Zeilen Logik. Das ist nicht nur kürzer, sondern auch lesbarer — der Code-Fluss konzentriert sich auf den Happy-Path, und die Error-Behandlung verschwindet hinter dem einen Zeichen.

Voraussetzungen am Rückgabe-Typ

Der ?-Operator funktioniert nicht überall — die umgebende Funktion muss einen passenden Rückgabe-Typ haben.

Rust Voraussetzungen
// OK: Funktion gibt Result zurück, ? auf Result
fn ok1() -> Result<i32, &'static str> {
    let r: Result<i32, &'static str> = Ok(42);
    let n = r?;
    Ok(n)
}

// Fehler: Funktion gibt nichts zurück
// fn nicht_ok() {
//     let r: Result<i32, &'static str> = Ok(42);
//     let n = r?;     // Compile-Fehler
// }

Der Compiler verlangt: wenn ? auf einem Result<T, E> angewendet wird, muss die umgebende Funktion entweder Result<U, F> (mit kompatiblem F) oder einen anderen Try-Typ zurückgeben. Sonst gibt es einen Compile-Fehler.

Diese Beschränkung ist semantisch nötig: ? macht ein Early-Return mit dem Error. Wenn die Funktion keinen Result-Typ hat, kann sie den Error nicht zurückgeben. Bei einer Funktion mit () (kein Rückgabe-Wert) gäbe es keinen Weg, den Fehler zu kommunizieren.

In fn main() kannst du ? ebenfalls nicht direkt verwenden — außer du gibst der main-Funktion einen Result-Rückgabe-Typ:

Rust main mit Result
use std::fs;

fn main() -> Result<(), std::io::Error> {
    let inhalt = fs::read_to_string("/etc/hostname")?;
    println!("Hostname: {}", inhalt.trim());
    Ok(())
}

fn main() -> Result<(), Box<dyn std::error::Error>> ist eine sehr typische Form für CLI-Programme: ? funktioniert überall, Fehler werden mit Debug-Output ausgegeben, der Exit-Code ist 1 bei Fehler und 0 bei Erfolg. Mehr im anyhow-Artikel und im Strategie-Artikel.

Automatische From-Konvertierung

Die wichtigste Eigenschaft des ?-Operators jenseits der reinen Propagation: er führt automatisch eine From-Konvertierung des Error-Typs durch.

Rust Verschiedene Error-Typen
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)
}

In dieser Funktion gibt es zwei verschiedene Quell-Fehler-Typen: std::io::Error von fs::read_to_string, ParseIntError von parse(). Die Funktion gibt aber Result<i32, AppError> zurück. Wie funktioniert das?

Der ?-Operator schaut nach einer From<QuellError> for ZielError-Implementation. Bei fs::read_to_string(pfad)? findet er From<std::io::Error> for AppError und ruft From::from(io_error) auf, was eine AppError::Io(io_error) produziert. Diese wird dann als Funktions-Return propagiert.

Damit kannst du verschiedene Stdlib-Fehler automatisch in deinen eigenen Error-Typ konvertieren, sobald du die From-Impls einmal definiert hast. Das macht Multi-Quell-Error-Handling extrem kompakt.

Was passiert ohne From-Impl?

Rust Ohne From
use std::fs;

// Ohne passende From-Impl: Compile-Fehler
// fn lese_einfach() -> Result<usize, String> {
//     let inhalt = fs::read_to_string("foo.txt")?;
//     // Fehler: `?` kann io::Error nicht zu String konvertieren
//     Ok(inhalt.len())
// }

// Mit map_err: explizite Konvertierung
fn lese_mit_map() -> Result<usize, String> {
    let inhalt = fs::read_to_string("foo.txt")
        .map_err(|e| format!("Lese-Fehler: {e}"))?;
    Ok(inhalt.len())
}

Wenn keine From-Implementation existiert, lehnt der Compiler den ?-Operator ab — mit einer Fehlermeldung, die explizit auf den fehlenden From-Trait hinweist. Du kannst dann entweder eine From-Impl schreiben (für wiederkehrende Konvertierungen) oder mit map_err eine inline-Konvertierung machen (für Einmal-Fälle).

?-Operator auf Option

Der ?-Operator funktioniert auch auf Option<T> — mit analoger Semantik:

Rust ? auf Option
fn ersten_buchstaben(s: &str) -> Option<char> {
    let erstes_wort = s.split_whitespace().next()?;
    erstes_wort.chars().next()
}

fn main() {
    assert_eq!(ersten_buchstaben("Hallo Welt"), Some('H'));
    assert_eq!(ersten_buchstaben(""), None);
}

Bei Option<T> macht ?: bei Some(x) wird x extrahiert; bei None wird return None ausgeführt. Voraussetzung: die Funktion gibt Option<U> zurück.

Damit kannst du Pipelines aus optional-fallible Operationen bauen, ohne in jedem Schritt Pattern-Match zu schreiben. Sehr typisch in Parsern, Lookups, Path-Traversal.

Mischen Result und Option im selben ?

Rust Konvertierung mit ok_or
fn parse_aus_map(map: &std::collections::HashMap<String, String>, key: &str) -> Result<i32, String> {
    let s = map.get(key).ok_or_else(|| format!("Key '{key}' fehlt"))?;
    let n: i32 = s.parse().map_err(|e| format!("kein i32: {e}"))?;
    Ok(n)
}

? funktioniert in einer Funktion immer mit einem Typ — entweder Result oder Option. Wenn du beide mischen willst (etwa Option-Lookup in einer Result-zurückgebenden Funktion), brauchst du eine Konvertierung. Option::ok_or(err) macht aus Option<T> ein Result<T, E>, danach funktioniert ? ganz normal.

Im Beispiel: map.get(key) liefert Option<&String>, .ok_or_else(...) macht es zu Result<&String, String>, ? propagiert das Err. Klassisches Pattern, wenn HashMap-Lookups in error-zurückgebenden Funktionen vorkommen.

Was passiert hinter den Kulissen

Der ?-Operator ist über den Try-Trait implementiert — eine Stdlib-Schnittstelle, die definiert, wie ein Typ Early-Return-Semantik unterstützt.

Rust Try-Trait Konzept
// Vereinfachte Darstellung:
// expr? expandiert grob zu:
//
// match Try::branch(expr) {
//     ControlFlow::Continue(wert) => wert,
//     ControlFlow::Break(residual) => return FromResidual::from_residual(residual),
// }

Die Details sind technisch und ändern sich gelegentlich, aber das mentale Modell ist: der ? ist syntaktischer Zucker für ein Match plus Early-Return. Der Try-Trait abstrahiert über verschiedene Typen — Result, Option, ControlFlow, und potenziell eigene Typen.

In stabilem Rust ist der Try-Trait nicht für eigene Typen verfügbar — du kannst aktuell keinen eigenen Typ definieren, auf dem ? funktioniert. Das ist als Nightly-Feature in Entwicklung und wird in einer zukünftigen Version stable werden. Bis dahin kannst du ? auf den drei genannten Stdlib-Typen verwenden.

Klassische Stolperfallen

Vergessenes ?

Rust Vergessen
use std::fs;

// Subtiler Bug: kein ? bedeutet, dass das Result NICHT propagiert wird
fn lese_vergessen(pfad: &str) -> String {
    let r = fs::read_to_string(pfad);
    // r ist ein Result, kein String — kompiliert nicht
    // r          // Compile-Fehler: mismatched types
    r.unwrap_or_default()       // Workaround mit Default
}

Ein häufiger Anfänger-Fehler: ? vergessen und dann verwirrt sein, warum der Code nicht kompiliert. Der Compiler hilft hier mit klarer Diagnose — er zeigt, dass ein Result da ist, wo ein String erwartet wird, und schlägt ? oder unwrap_or_default() vor.

Falscher Rückgabe-Typ

Rust Rückgabe-Typ-Mismatch
// Funktion mit ungeeignetem Rückgabe-Typ
fn falsch() -> i32 {
    let r: Result<i32, &str> = Ok(42);
    // let n = r?;            // Compile-Fehler: ? in einer Funktion ohne Result
    r.unwrap_or(0)
}

Wenn die Funktion keinen Result-Typ zurückgibt, schlägt ? fehl. Die Lösung: entweder den Rückgabe-Typ auf Result ändern oder eine andere Form der Fehler-Behandlung wählen (unwrap_or, expliziter Match).

Fehlende From-Impl

Rust Fehlende From
use std::num::ParseIntError;

#[derive(Debug)]
struct AppError;

// Ohne From<ParseIntError> for AppError:
// fn parse() -> Result<i32, AppError> {
//     let n: i32 = "abc".parse()?;       // Compile-Fehler
//     Ok(n)
// }

// Mit map_err — funktioniert
fn parse_mit_map() -> Result<i32, AppError> {
    let n: i32 = "abc".parse().map_err(|_: ParseIntError| AppError)?;
    Ok(n)
}

Wenn die Fehler-Typen nicht zusammenpassen und keine From-Impl existiert, gibt es einen Compile-Fehler. Die Lösung: entweder eine From-Implementation schreiben (gut für wiederkehrende Konvertierungen, siehe nächster Artikel) oder map_err für inline-Konvertierung verwenden.

Praxis: ?-Operator im echten Code

Datei-Verarbeitung

Rust Multi-Step-IO
use std::fs;
use std::io;

pub fn kopiere_und_zaehle(quelle: &str, ziel: &str) -> io::Result<usize> {
    let inhalt = fs::read_to_string(quelle)?;
    let zeilen = inhalt.lines().count();
    fs::write(ziel, &inhalt)?;
    Ok(zeilen)
}

Drei I/O-Operationen, jeweils potenziell fallible. Mit ? läuft jede Operation nacheinander durch; bei einem Fehler wird die Funktion sofort verlassen, der Aufrufer bekommt den Fehler.

Parser mit ?-Kette

Rust Header-Parser
fn parse_header(zeile: &str) -> Result<(String, String), String> {
    let pos = zeile.find(':').ok_or("kein Doppelpunkt")?;
    let name = zeile[..pos].trim().to_string();
    if name.is_empty() {
        return Err("leerer Name".into());
    }
    let wert = zeile[pos + 1..].trim().to_string();
    Ok((name, wert))
}

find(':').ok_or("...") macht aus Option<usize> ein Result<usize, &str>, der ? propagiert ggf. den Fehler. Die explizite Domain-Validierung (name.is_empty()) nutzt direkten return Err.

Result-zu-Option-Konvertierung in Pipelines

Rust filter_map
fn main() {
    let raw = ["1", "abc", "3", "x", "5"];
    // Mit ok() — Result → Option, dann filter_map
    let valid: Vec<i32> = raw.iter()
        .filter_map(|s| s.parse::<i32>().ok())
        .collect();
    assert_eq!(valid, vec![1, 3, 5]);
}

In Iterator-Pipelines ist ? selten direkt einsetzbar (Iteratoren haben keinen Result-Rückgabe-Typ). Stattdessen .ok() für die Konvertierung zu Option, dann filter_map für die Sammlung der erfolgreichen Werte.

main mit Result-Rückgabe

Rust CLI-main
use std::fs;
use std::env;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args: Vec<String> = env::args().collect();
    let pfad = args.get(1).ok_or("Bitte Pfad angeben")?;
    let inhalt = fs::read_to_string(pfad)?;
    let zahl: i32 = inhalt.trim().parse()?;
    println!("Wert: {zahl}");
    Ok(())
}

Eine typische CLI-main-Signatur. Box<dyn std::error::Error> ist ein „accepts everything"-Error-Typ — durch die Blanket-Impl von From<E: Error> werden alle Standard-Fehler-Typen automatisch konvertiert. Der ? funktioniert auf jedem fallible-Aufruf in der Funktion.

Multi-Source-Validierung

Rust Validierte Pipeline
#[derive(Debug)]
struct UserInput { name: String, age: i32 }

#[derive(Debug)]
enum ValidationError {
    NameLeer,
    AlterUngueltig(String),
    AlterAusserhalb(i32),
}

fn parse_und_validiere(raw_name: &str, raw_age: &str) -> Result<UserInput, ValidationError> {
    let name = raw_name.trim();
    if name.is_empty() {
        return Err(ValidationError::NameLeer);
    }

    let age: i32 = raw_age.trim().parse()
        .map_err(|e: std::num::ParseIntError| ValidationError::AlterUngueltig(e.to_string()))?;

    if !(0..=120).contains(&age) {
        return Err(ValidationError::AlterAusserhalb(age));
    }

    Ok(UserInput { name: name.to_string(), age })
}

Mischung aus direktem return Err (Domain-Validierung), map_err (Stdlib-Fehler in Domain-Fehler umwandeln), und ? (Propagation). Die Funktion ist immer noch linear lesbar — kein Indent-Aufstieg.

Verschachtelte ?

Rust Nested
use std::fs;
use std::path::Path;

fn lade_konfig_aus_verzeichnis(dir: &Path) -> std::io::Result<String> {
    let pfad = dir.join("config.toml");
    let inhalt = fs::read_to_string(&pfad)?;
    // Verschachtelt mit weiteren ?:
    let basis_dir = pfad.parent()
        .ok_or_else(|| std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            "Pfad hat kein Parent-Verzeichnis"
        ))?;
    println!("Geladen aus: {}", basis_dir.display());
    Ok(inhalt)
}

Option::ok_or_else mit ? zur Konvertierung von Option zu Result, im selben Funktions-Body wie andere fallible Operationen. Alles in einer kohärenten Fehler-Behandlung.

Result-Rückgabe in einer Closure

Rust Closure mit ?
fn main() -> Result<Vec<i32>, std::num::ParseIntError> {
    let inputs = ["1", "2", "3"];
    // Closure mit Result-Rückgabe — ? funktioniert intern
    let result: Result<Vec<i32>, _> = inputs.iter()
        .map(|s| {
            let n: i32 = s.parse()?;
            Ok(n * 2)
        })
        .collect();
    result
}

? funktioniert auch in Closures, sofern die Closure einen Result-Rückgabe-Typ hat. Bei der Iterator-Verarbeitung kann das nützlich sein — die Closure verarbeitet ein Element, ggf. mit Sub-Fehlern, der collect::<Result<Vec, E>>() sammelt das atomar.

Besonderheiten

? ist syntaktischer Zucker für Match plus Early-Return.

Bei Ok(x) wird x extrahiert. Bei Err(e) wird return Err(From::from(e)) ausgeführt. Das ist die ganze Magie. Aber es spart in der Praxis dramatisch viel Boilerplate.

Funktion muss Result, Option oder ControlFlow zurückgeben.

Der ?-Operator funktioniert nur in Funktionen mit einem Try-Typ als Rückgabe. In einer fn foo() -> i32 oder einer Funktion ohne Rückgabe-Typ schlägt ? fehl.

? ruft automatisch From::from auf dem Fehler.

Das ist das mächtigste Feature des Operators. Wenn der Quell-Fehler nicht direkt dem Ziel-Fehler entspricht, sucht der Compiler nach einer From-Impl und nutzt sie. Damit werden Multi-Quell-Fehler-Funktionen trivial.

main kann Result zurückgeben.

fn main() -> Result<(), Box<dyn Error>> ist die idiomatische Form für CLI-Programme. Der ?-Operator funktioniert überall, und Fehler werden mit Debug-Output und Exit-Code 1 behandelt.

Option::ok_or für Option-zu-Result-Konvertierung.

Wenn du Option-basierte Operationen in einer Result-zurückgebenden Funktion verwenden willst, brauchst du ok_or oder ok_or_else. Damit wird aus None ein Err mit deinem gewählten Fehler-Wert, und ? funktioniert wie üblich.

? auf Option macht Early-Return mit None.

Analog zu Result, aber für Option. Funktion muss Option zurückgeben. Praktisch in Funktionen, die selbst optionale Werte berechnen und auf Option-basierte Operationen zugreifen.

map_err als Alternative ohne From-Impl.

Wenn keine From-Impl existiert (oder du sie nicht schreiben willst), nutze expr.map_err(|e| ZielError::from(e))?. Inline-Konvertierung. Bei wiederkehrenden Konvertierungen lohnt sich eine echte From-Impl.

? funktioniert in Closures mit Result-Rückgabe.

Closures, die Result zurückgeben, unterstützen ?. Klassisch in Iterator::map, wenn die Map-Operation fehlbar ist, und du anschließend mit collect::<Result<Vec, E>>() sammeln willst.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Error Handling

Zur Übersicht