Thiserror: Effektives Fehlermanagement in Rust
Grace Collins
Solutions Engineer · Leapcell

Fehlerbehandlung
In der Programmierung ist die Fehlerbehandlung ein entscheidender Bestandteil. In Rust verwenden wir oft die Typen Result
und Option
zur Fehlerbehandlung. Manchmal müssen wir jedoch benutzerdefinierte Fehlertypen erstellen. Hier kommt die thiserror
-Crate ins Spiel, die den Code erheblich vereinfacht. Am Ende des Artikels gibt es einen Vergleich zwischen der Verwendung von thiserror
und der Nichtverwendung.
Überblick über die thiserror
-Crate
Das Hauptziel der thiserror
-Crate ist es, die Erstellung und Handhabung von benutzerdefinierten Fehlern in Rust zu vereinfachen. Um thiserror
in Ihrem Projekt zu verwenden, fügen Sie es zuerst zu Ihrer Cargo.toml
hinzu:
[dependencies] thiserror = "1.0"
Erstellung benutzerdefinierter Fehler
Die thiserror
-Crate bietet Entwicklern durch die Kombination von Rusts derive
-Makros und benutzerdefinierten Attributen die Möglichkeit, schnell benutzerdefinierte Fehlertypen zu erstellen.
Beispiel:
use thiserror::Error; // Definition eines benutzerdefinierten Fehlertyps #[derive(Error, Debug)] pub enum MyError { // Beschreibung für DataNotFound-Fehler #[error("Daten nicht gefunden")] DataNotFound, // Beschreibung für InvalidInput-Fehler #[error("ungültige Eingabe")] InvalidInput, } // Beispielfunktion, die zeigt, wie benutzerdefinierte Fehler verwendet werden fn search_data(query: &str) -> Result<(), MyError> { if query.is_empty() { // Gib InvalidInput-Fehler zurück, wenn die Abfrage leer ist return Err(MyError::InvalidInput); } // Die eigentliche Datenabfragelogik wird hier ausgelassen // ... // Gib DataNotFound-Fehler zurück, wenn Daten nicht gefunden werden Err(MyError::DataNotFound) }
Hier ist MyError
der benutzerdefinierte Fehler-Enum, den wir definiert haben. Jede Variable ist mit einem Attribut #[error("...")]
versehen, das die Meldung bereitstellt, die angezeigt werden soll, wenn der Fehler ausgelöst wird.
Verschachtelte Fehler
Die Fehlerverkettung ermöglicht das Erfassen und Reagieren auf Fehler, die von zugrunde liegenden Bibliotheken oder Funktionen weitergegeben werden. thiserror
bietet eine Möglichkeit, anzugeben, dass ein Fehler durch einen anderen Fehler verursacht wird.
Beispiel:
use std::io; use thiserror::Error; // Definition eines benutzerdefinierten Fehlertyps #[derive(Error, Debug)] pub enum MyError { // Beschreibung für IoError, das einen verschachtelten io::Error enthält #[error("I/O-Fehler ist aufgetreten")] IoError(#[from] io::Error), } // Beispielfunktion, die zeigt, wie verschachtelte Fehler verwendet werden fn read_file(file_path: &str) -> Result<String, MyError> { // Wenn fs::read_to_string einen Fehler zurückgibt, verwenden wir MyError::from, um ihn in MyError::IoError zu konvertieren std::fs::read_to_string(file_path).map_err(MyError::from) }
Das Attribut #[from]
gibt an, dass ein io::Error
automatisch in MyError::IoError
konvertiert werden kann.
Dynamische Fehlermeldungen
Dynamische Fehlermeldungen ermöglichen das Generieren von Fehlermeldungen basierend auf Laufzeitdaten.
Beispiel:
use thiserror::Error; // Definition eines benutzerdefinierten Fehlertyps #[derive(Error, Debug)] pub enum MyError { // Beschreibung für FailedWithCode, wobei {0} dynamisch durch den tatsächlichen Codewert ersetzt wird #[error("fehlgeschlagen mit Code: {0}")] FailedWithCode(i32), } // Beispielfunktion, die zeigt, wie dynamische Fehlermeldungen verwendet werden fn process_data(data: &str) -> Result<(), MyError> { let error_code = 404; // Einige berechnete Fehlercodes // Verwenden Sie den dynamischen error_code, um einen FailedWithCode-Fehler zu erstellen Err(MyError::FailedWithCode(error_code)) }
Bibliotheks- und Modulübergreifende Fehlerbehandlung
thiserror
unterstützt auch die automatische Konvertierung von anderen Fehlertypen. Dies ist besonders nützlich für die Fehlerbehandlung über Module oder Bibliotheken hinweg.
Beispiel:
use thiserror::Error; // Simulierter Fehlertyp, der aus einer anderen Bibliothek importiert wurde #[derive(Debug, Clone)] pub struct OtherLibError; // Definition eines benutzerdefinierten Fehlertyps #[derive(Error, Debug)] pub enum MyError { // Beschreibung für OtherError, der direkt von seinem inneren Fehlertyp erbt #[error(transparent)] OtherError(#[from] OtherLibError), } // Beispielfunktion, die zeigt, wie von einem anderen Fehlertyp konvertiert wird fn interface_with_other_lib() -> Result<(), MyError> { // Rufe eine Funktion aus einer anderen Bibliothek auf... // Wenn diese Funktion einen Fehler zurückgibt, verwenden wir MyError::from, um ihn in MyError::OtherError zu konvertieren Err(MyError::from(OtherLibError)); }
Das Attribut #[error(transparent)]
bedeutet, dass dieser Fehler lediglich als Container für einen anderen Fehler dient und seine Fehlermeldung direkt von seinem "Quell"-Fehler übernommen wird.
Vergleich mit anderen Fehlerbehandlungs-Crates
Obwohl thiserror
sehr nützlich ist, ist es nicht die einzige verfügbare Fehlerbehandlungs-Crate. Zum Beispiel ist anyhow
eine weitere beliebte Crate, die für schnelles Prototyping und Anwendungsentwicklung verwendet wird. thiserror
bietet jedoch flexiblere Fehlerdefinitionen und Mustervergleichsfunktionen.
Praktischer Fall
Betrachten Sie einen Vorgang, der das Lesen und Parsen einer Datei beinhaltet. Wir müssen potenzielle I/O-Fehler und Parsing-Fehler behandeln.
Beispiel:
use std::fs; use thiserror::Error; // Simulierter Parse-Fehlertyp, der aus einem anderen Teil importiert wurde #[derive(Debug, Clone)] pub struct ParseDataError; // Definition eines benutzerdefinierten Fehlertyps #[derive(Error, Debug)] pub enum MyError { // Beschreibung für IoError, das einen verschachtelten io::Error enthält #[error("I/O-Fehler ist aufgetreten")] IoError(#[from] io::Error), // Beschreibung für ParseError, das einen verschachtelten ParseDataError enthält #[error("Daten konnten nicht geparst werden")] ParseError(#[from] ParseDataError), } // Liest eine Datei und versucht, ihren Inhalt zu parsen fn read_and_parse(filename: &str) -> Result<String, MyError> { // Liest den Dateiinhalt, kann einen I/O-Fehler auslösen let content = fs::read_to_string(filename)?; // Versucht, den Inhalt zu parsen, kann einen Parse-Fehler auslösen parse_data(&content).map_err(MyError::from) } // Simulierte Datenparsing-Funktion, die hier immer einen Fehler zurückgibt fn parse_data(content: &str) -> Result<String, ParseDataError> { Err(ParseDataError) } // Hauptfunktion, die demonstriert, wie die obige Fehlerbehandlungslogik verwendet wird fn main() { match read_and_parse("data.txt") { Ok(data) => println!("Daten: {}", data), Err(e) => eprintln!("Fehler: {}", e), } }
Vergleich: Verwendung von thiserror
vs. Nicht-Verwendung von thiserror
Betrachten wir ein komplexeres Beispiel mit mehreren möglichen Fehlern, die aus mehreren Quellen stammen.
Angenommen, Sie schreiben eine Anwendung, die Daten von einer Remote-API abrufen und die Daten dann in einer Datenbank speichern muss. Jeder Schritt kann fehlschlagen und verschiedene Arten von Fehlern zurückgeben.
Code ohne Verwendung von thiserror
:
use std::fmt; #[derive(Debug)] enum DataFetchError { HttpError(u16), Timeout, InvalidPayload, } impl fmt::Display for DataFetchError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::HttpError(code) => write!(f, "HTTP error with code: {}", code), Self::Timeout => write!(f, "Data fetching timed out"), Self::InvalidPayload => write!(f, "Invalid payload received"), } } } impl std::error::Error for DataFetchError {} #[derive(Debug)] enum DatabaseError { ConnectionFailed, WriteFailed(String), } impl fmt::Display for DatabaseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::ConnectionFailed => write!(f, "Failed to connect to database"), Self::WriteFailed(reason) => write!(f, "Failed to write to database: {}", reason), } } } impl std::error::Error for DatabaseError {}
Code mit Verwendung von thiserror
:
use thiserror::Error; #[derive(Debug, Error)] enum DataFetchError { #[error("HTTP error with code: {0}")] HttpError(u16), #[error("Data fetching timed out")] Timeout, #[error("Invalid payload received")] InvalidPayload, } #[derive(Debug, Error)] enum DatabaseError { #[error("Failed to connect to database")] ConnectionFailed, #[error("Failed to write to database: {0}")] WriteFailed(String), }
Analyse
- Reduzierter Code: Für jeden Fehlertyp benötigen wir keine separaten
Display
- undError
-Trait-Implementierungen mehr. Dies reduziert den Boilerplate-Code erheblich und verbessert die Lesbarkeit des Codes. - Fehlermeldungen befinden sich in der Nähe von Definitionen: Mit
thiserror
können wir die Fehlermeldung direkt neben die Fehlerdefinition schreiben. Dies macht den Code übersichtlicher und einfacher zu finden und zu ändern. - Erhöhte Wartbarkeit: Wenn wir Fehlertypen hinzufügen oder entfernen müssen, müssen wir nur die Enum-Definition ändern und die Fehlermeldungen aktualisieren, ohne andere Teile des Codes ändern zu müssen.
Je komplexer unsere Fehlertypen und Szenarien werden, desto deutlicher werden die Vorteile der Verwendung von thiserror
.
Wir sind Leapcell, Ihre erste Wahl für das Hosten von Rust-Projekten.
Leapcell ist die Next-Gen Serverless Platform für Webhosting, Async Tasks und Redis:
Multi-Language Support
- Entwickeln Sie mit Node.js, Python, Go oder Rust.
Deployen Sie unbegrenzt viele Projekte kostenlos
- Zahlen Sie nur für die Nutzung — keine Anfragen, keine Gebühren.
Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 $ unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für müheloses Setup.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und Protokollierung für verwertbare Erkenntnisse.
Mühelose Skalierbarkeit und hohe Leistung
- Auto-Scaling zur mühelosen Bewältigung hoher Parallelität.
- Kein operativer Overhead — konzentrieren Sie sich einfach auf das Bauen.
Erfahren Sie mehr in der Dokumentation!
Folgen Sie uns auf X: @LeapcellHQ