1. Error handling
Error handling is a crucial part of programming. In Rust, we use it oftenResult
andOption
type for error handling. But sometimes, we need to create a custom error type. This isthiserror
Where the library works, it can greatly simplify the code, and there is a comparison between using thiserror and not using it at the end of the article.
2. Overview of thiserror library
thiserror
The main goal of the library is to simplify custom error creation and handling in Rust. To use it in your projectthiserror
, first inAdded in:
tomlCopy code [dependencies] thiserror = "1.0"
3. Create a custom error
thiserror
The library is combined with Rust'sderive
Macros and custom properties provide developers with the ability to quickly create custom error types.
Example:
use thiserror::Error; // Customize the definition of error type#[derive(Error, Debug)] pub enum MyError { // DataNotFound error description #[error("data not found")] DataNotFound, // InvalidInput error description #[error("invalid input")] InvalidInput, } // Sample function showing how to use custom errorsfn search_data(query: &str) -> Result<(), MyError> { if query.is_empty() { // When the query is empty, return an InvalidInput error return Err(MyError::InvalidInput); } // The actual data query logic is omitted here // ... // Return DataNotFound error when data is not found Err(MyError::DataNotFound) }
Here,MyError
is the custom error enumeration we define. Next to each variable#[error("...")]
The property provides the message that should be displayed when the error is triggered.
4. Nesting errors
Error chains allow to catch and respond to errors propagated from underlying libraries or functions.thiserror
Provides a way to specify that an error is caused by another error.
Example:
use std::io; use thiserror::Error; // Customize the definition of error type#[derive(Error, Debug)] pub enum MyError { // IoError error description, which contains a nested io::Error #[error("I/O error occurred")] IoError(#[from] io::Error), } // Sample function showing how to use nested errorsfn read_file(file_path: &str) -> Result<String, MyError> { // If fs::read_to_string returns an error, we use MyError::from to convert it to MyError::IoError std::fs::read_to_string(file_path).map_err(MyError::from) }
#[from]
Attribute tags meanio::Error
Can be automatically converted toMyError::IoError
。
5. Dynamic error message
Dynamic error messages allow error messages to be generated based on runtime data.
Example:
use thiserror::Error; // Customize the definition of error type#[derive(Error, Debug)] pub enum MyError { // FailedWithCode error description, where {0} will be dynamically replaced with specific code value #[error("failed with code: {0}")] FailedWithCode(i32), } // Sample function showing how to use dynamic error messagesfn process_data(data: &str) -> Result<(), MyError> { let error_code = 404; // Some calculated error codes // Create FailedWithCode with dynamic error_code Err(MyError::FailedWithCode(error_code)) }
6. Error handling across libraries and modules
thiserror
Automatic conversion from other error types is also supported. This is especially useful in cross-module or cross-library error handling.
Example:
use thiserror::Error; // Simulate error types imported from other libraries#[derive(Debug, Clone)] pub struct OtherLibError; // Customize the definition of error type#[derive(Error, Debug)] pub enum MyError { // OtherError description, which inherits directly from the error type inside it #[error(transparent)] OtherError(#[from] OtherLibError), } // Sample function showing how to convert from other error typesfn interface_with_other_lib() -> Result<(), MyError> { // Call functions from other libraries... // If that function returns an error, we use MyError::from to convert it to MyError::OtherError Err(MyError::from(OtherLibError)) }
#[error(transparent)]
The property means that the error is just a container for other errors, and its error messages will be inherited directly from its "source" error.
7. Compare other error handling libraries
Althoughthiserror
Very useful, but it is not the only error handling library. For example,anyhow
is another popular library for rapid prototyping and application. butthiserror
Provides more flexible error definition and pattern matching capabilities.
8. Actual cases
Consider a file reading and parsing operation. We need to deal with possible I/O errors and parsing errors.
Example:
use std::fs; use thiserror::Error; // Simulate parsing error types imported from other parts#[derive(Debug, Clone)] pub struct ParseDataError; // Customize the definition of error type#[derive(Error, Debug)] pub enum MyError { // IoError error description, which contains a nested io::Error #[error("I/O error occurred")] IoError(#[from] io::Error), // ParseError error description, which contains a nested ParseDataError #[error("failed to parse data")] ParseError(#[from] ParseDataError), } // Read the file and try to parse its contentsfn read_and_parse(filename: &str) -> Result<String, MyError> { // Reading the file content may throw an I/O error let content = fs::read_to_string(filename)?; // Trying to parse the content may throw a parse error parse_data(&content).map_err(MyError::from) } // The simulated data analysis function always returns an error herefn parse_data(content: &str) -> Result<String, ParseDataError> { Err(ParseDataError) } // Main function, showing how to use the above error handling logicfn main() { match read_and_parse("") { Ok(data) => println!("Data: {}", data), Err(e) => eprintln!("Error: {}", e), } }
9. Compare this error and not this error
Let's consider a more complex example that involves multiple possible errors generated from multiple sources.
Suppose you are writing an application that needs to fetch data from a remote API and then save the data to a database. Each step may fail and return a different error.
Code without using 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 using 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), }
analyze:
-
Reduced code: For each error type, we no longer need a separate one
Display
andError
trait implementation. This greatly reduces boilerplate code and improves the readability of the code. -
Error message with definition: use
thiserror
, we can write an error message directly next to the error definition. This makes the code more organized and convenient for finding and modifying. - Increased maintenance: If we want to add or delete the error type, we just need to modify the enum definition and update the error message, without making changes elsewhere.
In this way, when our error types and scenarios become more complex,thiserror
The advantages are revealed.
The above is a detailed explanation of the use of thiserror library in Rust. For more information about the use of the Rust thiserror library, please follow my other related articles!