Fehlerbehandlung in Rust: Von unwrap bis zu fortgeschrittenen Techniken
Im Gegensatz zu Sprachen mit Ausnahmen (Java, Python, C++) verwendet Rust keinen try/catch-Mechanismus. Stattdessen bietet die Sprache ein leistungsstarkes und sicheres Typsystem zur Fehlerbehandlung. Anfänger neigen oft dazu, unwrap() übermäßig zu verwenden, was zu Programmabstürzen führt. In diesem Artikel betrachten wir fortgeschrittene Techniken zur Arbeit mit Fehlern in Rust: von der Erstellung eigener Fehlertypen bis hin zur Verwendung der Crates thiserror und anyhow, und analysieren, wie das Verhalten von try/catch emuliert werden kann.
1. Warum hat Rust auf try/catch verzichtet?
In Sprachen mit Ausnahmen kann der Ausführungsfluss jederzeit unterbrochen werden. Dies erschwert die Codeanalyse und kann zu Ressourcenlecks führen. Rust bietet zwei grundlegende Ansätze:
- Behebbare Fehler — der Typ
Result<T, E>(ErfolgOk(T)oder FehlerErr(E)). - Unbehebbare Fehler — das Makro
panic!, das das Programm anhält.
Der ?-Operator ist das Rust-Äquivalent zu try. Er extrahiert den Wert aus Ok oder gibt sofort Err aus der Funktion zurück.
use std::fs::File;use std::io::Read;
fn read_username(path: &str) -> Result<String, std::io::Error> { let mut file = File::open(path)?; // bei Fehler — geben wir ihn zurück let mut username = String::new(); file.read_to_string(&mut username)?; // erneut "try" Ok(username.trim().to_string())}2. Erstellung eigener Fehlertypen
Für komplexe Projekte reicht ein einzelner std::io::Error nicht aus. Die beste Praxis ist es, einen eigenen Fehlertyp mit einer Aufzählung (enum) zu definieren. Dies ermöglicht die Gruppierung verschiedener Fehlerarten und das Hinzufügen von Kontext.
use std::fmt;
#[derive(Debug)]enum MyAppError { IoError(std::io::Error), ParseError(String), NetworkError { url: String, status: u16 },}
impl fmt::Display for MyAppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { MyAppError::IoError(e) => write!(f, "IO-Fehler: {}", e), MyAppError::ParseError(s) => write!(f, "Parse-Fehler: {}", s), MyAppError::NetworkError { url, status } => { write!(f, "Netzwerkfehler für {}: Status {}", url, status) } } }}
impl std::error::Error for MyAppError {} // implementiert das Error-TraitJetzt kann ? für die automatische Konvertierung verwendet werden, wenn das From-Trait implementiert wird:
impl From<std::io::Error> for MyAppError { fn from(err: std::io::Error) -> MyAppError { MyAppError::IoError(err) }}
fn process_file(path: &str) -> Result<(), MyAppError> { let content = std::fs::read_to_string(path)?; // wird automatisch konvertiert if content.is_empty() { return Err(MyAppError::ParseError("leere Datei".into())); } Ok(())}3. Fortgeschrittene Crates: thiserror und anyhow
Es ist mühsam, alle Display- und From-Implementierungen manuell zu schreiben. Dafür gibt es zwei beliebte Crates:
thiserror — für Bibliotheken
Das Crate thiserror generiert die Implementierung von std::error::Error über ein derive-Makro. Ideal für Bibliotheken, die eine präzise Fehlerbeschreibung benötigen.
use thiserror::Error;
#[derive(Error, Debug)]pub enum DataError { #[error("IO-Fehler: {0}")] Io(#[from] std::io::Error), #[error("Ungültiges UTF-8: {0}")] Utf8(#[from] std::str::Utf8Error), #[error("Benutzerdefinierter Fehler: {details}")] Custom { details: String },}
// Jetzt kann man schreiben:fn parse_data(bytes: &[u8]) -> Result<String, Da