thiserror is a popular Rust crate that simplifies the process of creating custom error types by providing a `#[derive(Error)]` procedural macro. Its primary goal is to reduce the boilerplate code associated with implementing the standard library's `std::error::Error` trait, `std::fmt::Display` trait, and optionally the `std::fmt::Debug` trait for custom errors.
Purpose and Benefits:
1. Boilerplate Reduction: It automates the implementation of `Display` and `Error` traits, allowing developers to focus on defining the error conditions rather than the trait details.
2. Readability and Clarity: Error messages are defined directly on the enum variants using a `format!`-like syntax, making error definitions clear and concise.
3. Automatic Conversions (`#[from]`): It provides the `#[from]` attribute, which automatically implements `From<SourceError>` for your custom error. This is crucial for using the `?` operator to propagate errors from other libraries directly into your custom error type.
4. Error Chaining (`#[source]`): The `#[source]` attribute allows you to designate a field within an error variant as the underlying cause of the error. This implements the `source()` method of the `std::error::Error` trait, enabling robust error introspection and debugging.
5. Integration with Standard Library: `thiserror` types fully conform to Rust's standard error handling mechanisms, making them compatible with tools like `anyhow` for application-level error handling.
6. Type Safety: Encourages defining explicit, strongly-typed errors, which improves maintainability and robustness in larger codebases.
Key Attributes:
* `#[derive(Error)]`: The main macro that implements the `Error` and `Display` traits for your enum or struct.
* `#[error("...")]`: Used on enum variants or struct fields to define the user-facing display message. It supports `format!`-like placeholders that reference fields of the variant (e.g., `#[error("Key '{key}' not found")]`).
* `#[from]`: Used on a field in an enum variant to automatically implement `From<SourceError>` for that variant. The `SourceError` type is inferred from the field's type.
* `#[source]`: Used on a field in an enum variant to indicate that this field represents the underlying cause of the error. The `std::error::Error::source()` method will return a reference to this field, allowing for error chain introspection.
Usage Pattern:
Typically, `thiserror` is used by library authors to define precise, domain-specific error types that can be returned by their library functions. Application authors can then use these specific error types or convert them into a generic `anyhow::Error` for simpler top-level error handling.
Example Code
```rust
// Cargo.toml
// [package]
// name = "thiserror_example"
// version = "0.1.0"
// edition = "2021"
//
// [dependencies]
// thiserror = "1.0"
// src/main.rs
use thiserror::Error;
use std::{fmt, fs, io, num::ParseIntError};
/// Define our custom application error type using `thiserror`.
#[derive(Error, Debug)]
pub enum AppError {
/// A variant for I/O errors.
/// `#[from]` automatically implements `From<std::io::Error>` for `AppError`.
/// `#[error(...)]` defines the display message for this error variant.
#[error("File I/O error: {0}")]
Io(#[from] io::Error),
/// A variant for integer parsing errors.
/// `#[from]` automatically implements `From<std::num::ParseIntError>` for `AppError`.
#[error("Failed to parse integer from string: {0}")]
Parse(#[from] ParseIntError),
/// A custom error variant with specific fields.
/// The `#[error(...)]` macro uses `format!`-like syntax, referencing enum fields.
#[error("Configuration key '{key}' not found")]
ConfigNotFound { key: String },
/// An error variant that wraps another error as its source.
/// `#[source]` designates the field that provides the underlying error. This is useful
/// for wrapping more specific library errors or providing additional context.
#[error("Invalid path '{path}': {detail}")]
InvalidPath {
path: String,
#[source] // This field is the source of the error, accessible via `Error::source()`
detail: io::Error, // An underlying I/O error that caused the path to be invalid
},
}
/// Function that might return an `Io` error.
fn read_application_config(path: &str) -> Result<String, AppError> {
let content = fs::read_to_string(path)?; // `?` operator converts `io::Error` to `AppError::Io`
Ok(content)
}
/// Function that might return a `Parse` error.
fn get_user_id_from_string(id_str: &str) -> Result<u32, AppError> {
let id = id_str.parse::<u32>()?; // `?` operator converts `ParseIntError` to `AppError::Parse`
Ok(id)
}
/// Function that might return a `ConfigNotFound` error.
fn get_config_value(config_content: &str, key: &str) -> Result<String, AppError> {
// Simulate finding a key in some config content
if config_content.contains(key) {
Ok(format!("Found value for {}", key))
} else {
Err(AppError::ConfigNotFound { key: key.to_string() })
}
}
/// Function that returns `InvalidPath` error with a source.
fn validate_file_path(path: &str) -> Result<(), AppError> {
if path.contains("bad_segment") {
// Simulate an underlying I/O error that makes the path invalid
Err(AppError::InvalidPath {
path: path.to_string(),
detail: io::Error::new(io::ErrorKind::InvalidInput, "Path contains prohibited segments"),
})
} else {
Ok(())
}
}
fn main() -> Result<(), AppError> {
println!("--- Demonstrating `thiserror` examples ---");
// 1. Demonstrate `Io` error (from `std::io::Error` via `#[from]`)
println!("\nAttempting to read a non-existent file...");
match read_application_config("non_existent_file.txt") {
Ok(_) => println!("Config read successfully."),
Err(e) => eprintln!("Error: {}", e), // Uses `Display` impl provided by `thiserror`
}
// 2. Demonstrate `Parse` error (from `std::num::ParseIntError` via `#[from]`)
println!("\nAttempting to parse an invalid integer string...");
match get_user_id_from_string("not_a_number") {
Ok(id) => println!("User ID: {}", id),
Err(e) => eprintln!("Error: {}", e),
}
// 3. Demonstrate `ConfigNotFound` error (custom error variant)
println!("\nAttempting to get a missing config value...");
let config_content = "username=admin\npassword=secret";
match get_config_value(config_content, "api_key") {
Ok(value) => println!("Config value: {}", value),
Err(e) => eprintln!("Error: {}", e),
}
// 4. Demonstrate `InvalidPath` error with `#[source]`
println!("\nAttempting to validate a path with a bad segment...");
match validate_file_path("/home/user/bad_segment/config.txt") {
Ok(_) => println!("Path validated successfully."),
Err(e) => {
eprintln!("Error: {}", e);
// Access the underlying source error if available
if let Some(source) = e.source() {
eprintln!(" Caused by: {}", source); // The `source` field's `Display` impl
// You can even downcast the source if you know its concrete type
if let Some(io_err) = source.downcast_ref::<io::Error>() {
eprintln!(" (Specifically an I/O error kind: {:?})", io_err.kind());
}
}
}
}
println!("\n--- Demonstrating successful operations ---");
// Successful operations using `?` for propagation
read_application_config("Cargo.toml")?; // This should succeed if Cargo.toml exists
println!("Successfully read Cargo.toml.");
get_user_id_from_string("12345")?;
println!("Successfully parsed user ID '12345'.");
get_config_value(config_content, "username")?;
println!("Successfully retrieved username from config.");
validate_file_path("/etc/config/valid_config.conf")?;
println!("Successfully validated a clean path.");
Ok(())
}
```








thiserror