Loading page…
Rust walkthroughs
Loading page…
thiserror::Error derive macro generate std::error::Error implementations automatically?The thiserror::Error derive macro automates the boilerplate required to implement std::error::Error for custom error types, generating Display, Error, and optionally From implementations based on attribute annotations. This eliminates the repetitive pattern of writing error enums with manual trait implementations while preserving full type safety and error chaining capabilities.
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DataStoreError {
#[error("data store disconnected")]
Disconnect,
#[error("data corruption detected at offset {0}")]
Corrupted(usize),
#[error("permission denied for user {user_id}")]
PermissionDenied { user_id: u64 },
}
// The macro generates:
// - impl std::fmt::Display for DataStoreError
// - impl std::error::Error for DataStoreError
// - impl std::fmt::Debug for DataStoreError (via derive)The #[error(...)] attribute specifies the Display implementation for each variant.
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ParseError {
#[error("unexpected token: {0}")]
UnexpectedToken(String),
#[error("expected {expected}, found {found}")]
Mismatch { expected: String, found: String },
#[error("at line {line}, column {col}")]
Position { line: usize, col: usize },
}
fn demonstrate_display() {
let err = ParseError::UnexpectedToken("foo".to_string());
assert_eq!(err.to_string(), "unexpected token: foo");
let err = ParseError::Mismatch {
expected: "identifier".to_string(),
found: "number".to_string(),
};
assert_eq!(err.to_string(), "expected identifier, found number");
}The macro generates a match expression that formats each variant using the specified template.
use thiserror::Error;
use std::io;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("failed to read config file")]
Io(#[from] io::Error),
#[error("invalid config format")]
Parse(#[source] serde_json::Error),
#[error("config validation failed: {0}")]
Validation(String),
}
fn demonstrate_source() {
let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
let config_err = ConfigError::from(io_error);
// source() returns the underlying error
assert!(config_err.source().is_some());
}
fn demonstrate_from() {
// #[from] generates From<io::Error> for ConfigError
let io_error = io::Error::new(io::ErrorKind::PermissionDenied, "access denied");
let config_err: ConfigError = io_error.into();
// Automatically converts io::Error to ConfigError::Io
}The #[source] attribute marks fields that provide the error chain source.
use thiserror::Error;
use std::io;
#[derive(Error, Debug)]
pub enum AppError {
// #[from] generates From<io::Error> implementation
#[error("IO operation failed")]
Io(#[from] io::Error),
// Manual From implementation would be:
// impl From<io::Error> for AppError {
// fn from(err: io::Error) -> Self {
// AppError::Io(err)
// }
// }
}
fn use_from() -> Result<(), AppError> {
// ? operator works because From is implemented
let content = std::fs::read_to_string("config.txt")?;
Ok(())
}The #[from] attribute generates the From trait implementation automatically.
use thiserror::Error;
#[derive(Error, Debug)]
pub enum WrapperError {
#[error(transparent)]
Other(#[from] anyhow::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
}
// transparent forwards Display and Error implementations to inner type
// Useful when you want error types to be interchangeable
fn demonstrate_transparent() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
let wrapper = WrapperError::from(io_err);
// Display is forwarded, so this prints "not found"
assert_eq!(wrapper.to_string(), "not found");
}The #[error(transparent)] attribute forwards both Display and Error::source to the inner type.
use thiserror::Error;
use std::io;
#[derive(Error, Debug)]
pub enum MultiSourceError {
#[error("database error: {0}")]
Database(#[source] sqlx::Error),
#[error("cache error: {source}")]
Cache {
#[source]
source: redis::RedisError,
context: String,
},
#[error("IO error in {context}: {source}")]
IoContext {
#[source]
source: io::Error,
context: String,
},
}
fn demonstrate_multi_source() {
let io_err = io::Error::new(io::ErrorKind::Interrupted, "interrupted");
let err = MultiSourceError::IoContext {
source: io_err,
context: "file read".to_string(),
};
assert_eq!(err.to_string(), "IO error in file read: interrupted");
assert!(err.source().is_some());
}Use #[source] when the source field is not the first field in a struct variant.
use thiserror::Error;
#[derive(Error, Debug)]
#[error("operation failed for {resource}: {source}")]
pub struct OperationError {
pub resource: String,
#[source]
pub source: std::io::Error,
}
#[derive(Error, Debug)]
#[error("validation error: {message}")]
pub struct ValidationError {
pub field: String,
pub message: String,
}
fn demonstrate_struct_errors() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
let op_err = OperationError {
resource: "config.yaml".to_string(),
source: io_err,
};
assert_eq!(op_err.to_string(), "operation failed for config.yaml: file missing");
}Struct errors allow named fields with context information.
use thiserror::Error;
#[derive(Error, Debug)]
pub enum GenericError<T: std::fmt::Debug> {
#[error("processing error: {0}")]
Processing(T),
#[error("validation failed for {id}: {message}")]
Validation { id: u64, message: String },
}
// Generic types work with thiserror
// The Display bound is inferred from {0} usage
#[derive(Error, Debug)]
pub enum ServiceError<E: std::error::Error + 'static> {
#[error("service error: {0}")]
Inner(#[source] E),
#[error("timeout after {ms}ms")]
Timeout { ms: u64 },
}Generic error types require appropriate bounds for field formatting.
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Layer1Error {
#[error("layer 1 error: {0}")]
Message(String),
}
#[derive(Error, Debug)]
pub enum Layer2Error {
#[error("layer 2 failed")]
Layer1(#[from] Layer1Error),
#[error("layer 2 specific error")]
Specific,
}
#[derive(Error, Debug)]
pub enum Layer3Error {
#[error("layer 3 failed")]
Layer2(#[from] Layer2Error),
}
fn demonstrate_nested() {
let l1 = Layer1Error::Message("oops".to_string());
let l2: Layer2Error = l1.into();
let l3: Layer3Error = l2.into();
// Error chain: Layer3Error -> Layer2Error -> Layer1Error
assert!(l3.source().is_some());
}Nested error types create error chains through #[from] implementations.
use thiserror::Error;
#[derive(Error, Debug)]
pub enum BacktraceError {
#[error("error with backtrace")]
WithBacktrace {
#[source]
source: std::io::Error,
#[backtrace]
backtrace: std::backtrace::Backtrace,
},
#[error("error without backtrace")]
WithoutBacktrace(String),
}
// Note: #[backtrace] is available with Rust 1.65+
// The backtrace is captured at error creationThe #[backtrace] attribute captures stack traces at error creation time.
// Manual implementation
pub enum ManualError {
Io(std::io::Error),
Custom(String),
}
impl std::fmt::Display for ManualError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ManualError::Io(e) => write!(f, "IO error: {}", e),
ManualError::Custom(s) => write!(f, "Custom error: {}", s),
}
}
}
impl std::error::Error for ManualError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ManualError::Io(e) => Some(e),
ManualError::Custom(_) => None,
}
}
}
impl From<std::io::Error> for ManualError {
fn from(err: std::io::Error) -> Self {
ManualError::Io(err)
}
}
// Thiserror derivation
#[derive(thiserror::Error, Debug)]
pub enum DerivedError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Custom error: {0}")]
Custom(String),
}
// The macro generates all of the above automaticallyThe derive macro eliminates approximately 30-40 lines of boilerplate for a typical error enum.
use thiserror::Error;
use std::io;
#[derive(Error, Debug)]
pub enum ApiError {
#[error("database connection failed")]
Database(#[from] sqlx::Error),
#[error("cache operation failed: {0}")]
Cache(String),
#[error("invalid request: {field} - {reason}")]
Validation { field: String, reason: String },
#[error("authentication failed")]
Unauthorized,
#[error("rate limit exceeded, retry after {retry_after}s")]
RateLimited { retry_after: u64 },
#[error("internal server error")]
Internal(#[source] io::Error),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
impl ApiError {
pub fn status_code(&self) -> u16 {
match self {
ApiError::Database(_) => 500,
ApiError::Cache(_) => 500,
ApiError::Validation { .. } => 400,
ApiError::Unauthorized => 401,
ApiError::RateLimited { .. } => 429,
ApiError::Internal(_) => 500,
ApiError::Other(_) => 500,
}
}
}
// Usage in handlers
async fn handler() -> Result<String, ApiError> {
let user = validate_user().map_err(|e| ApiError::Validation {
field: "user".to_string(),
reason: e.to_string(),
})?;
Ok(format!("Hello, {}", user))
}A complete web service error type with automatic Display, Error, and From implementations.
use thiserror::Error;
#[derive(Error, Debug)]
pub enum VisibilityError {
// Public fields accessible in formatting
#[error("user {user_id} not found")]
UserNotFound { user_id: u64 },
// Private field with public formatting
#[error("internal error: {message}")]
Internal { message: String },
// Skipped in formatting, used only as source
#[error("IO error")]
Io(#[source] std::io::Error),
// Tuple variant with positional formatting
#[error("error code {0}: {1}")]
Code(u32, String),
}Fields can be used in formatting templates regardless of visibility.
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConditionalError {
#[error("standard error")]
Standard,
#[cfg(feature = "advanced")]
#[error("advanced error: {0}")]
Advanced(String),
#[cfg(feature = "async")]
#[error("async error")]
Async(#[from] tokio::task::JoinError),
}
// Feature-gated variants are only compiled when features are enabledFeature-gated error variants integrate with Cargo feature flags.
What the macro generates:
| Attribute | Generates |
|-----------|-----------|
| #[error(...)] | impl Display with formatted output |
| #[source] | fn source() returning the field |
| #[from] | impl From<SourceType> |
| #[error(transparent)] | Forwarded Display and source() |
| #[backtrace] | Backtrace captured at creation |
Macro expansion process:
#[error(...)] templates for each variantmatch expression for Display::fmtError::source for #[source] fieldsFrom for #[from] fieldstransparent, delegates all implementationsCommon patterns:
#[error(...)]#[from] for automatic conversions#[error(transparent)] for forwardingKey insight: The thiserror::Error derive macro transforms the verbose pattern of implementing Display, Error, and From into a declarative attribute system. Instead of writing repetitive match expressions and trait implementations, you annotate each variant with its error message format and source relationships. The macro generates all necessary code while preserving the ability to add custom methods, conversions, and functionality. This separation of error definition from error implementation makes error types easier to maintain and evolve, especially as applications grow and error handling requirements become more complex. The macro's design demonstrates Rust's capability to eliminate boilerplate through procedural macros while maintaining type safety and zero runtime overhead.