Loading page…
Rust walkthroughs
Loading page…
Thiserror is a procedural macro crate that simplifies creating custom error types that implement std::error::Error. It generates boilerplate code for Display, Error, and From implementations, letting you focus on defining error variants and their data. Thiserror integrates seamlessly with the ? operator and error handling patterns.
Key features:
std::error::Error and Display#[source] attribute#[error("...")]From for source typesThiserror is ideal for libraries that need domain-specific error types with proper error chaining.
# Cargo.toml
[dependencies]
thiserror = "1"use std::io;
use thiserror::Error;
// ===== Basic Error Definition =====
#[derive(Debug, Error)]
pub enum DataStoreError {
#[error("Configuration error: {0}")]
Config(String),
#[error("Data not found for key: {key}")
NotFound { key: String },
#[error("Invalid data format: expected {expected}, got {actual}")
InvalidFormat { expected: String, actual: String },
#[error("Operation timed out after {seconds} seconds")]
Timeout { seconds: u64 },
}
fn main() {
let err = DataStoreError::NotFound { key: "user:42".to_string() };
println!("Error: {}", err);
let err = DataStoreError::InvalidFormat {
expected: "JSON".to_string(),
actual: "XML".to_string(),
};
println!("Error: {}", err);
}use std::io;
use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum FileError {
#[error("Failed to read file '{path}'")]
ReadError {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("Failed to write to file '{path}'")]
WriteError {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("File '{path}' does not exist")]
NotFound { path: PathBuf },
#[error("Permission denied for file '{path}'")]
PermissionDenied { path: PathBuf },
}
fn read_config(path: &std::path::Path) -> Result<String, FileError> {
std::fs::read_to_string(path).map_err(|e| FileError::ReadError {
path: path.to_path_buf(),
source: e,
})
}
fn main() {
match read_config(std::path::Path::new("nonexistent.txt")) {
Ok(content) => println!("Content: {}", content),
Err(e) => {
println!("Error: {}", e);
if let Some(source) = e.source() {
println!("Caused by: {}", source);
}
}
}
}
use std::error::Error;use std::num::ParseIntError;
use std::str::Utf8Error;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ParseError {
#[error("Failed to parse integer")]
Int(#[from] ParseIntError),
#[error("Invalid UTF-8 encoding")]
Utf8(#[from] Utf8Error),
#[error("Invalid format at position {position}: {message}")]
Invalid { position: usize, message: String },
}
fn parse_number(s: &str) -> Result<i32, ParseError> {
// ParseIntError automatically converts to ParseError::Int
let n: i32 = s.parse()?;
Ok(n)
}
fn parse_and_double(s: &str) -> Result<i32, ParseError> {
let n = parse_number(s)?;
Ok(n * 2)
}
fn main() {
match parse_and_double("not a number") {
Ok(n) => println!("Result: {}", n),
Err(e) => println!("Error: {}", e),
}
}use std::time::Duration;
use thiserror::Error;
#[derive(Debug, Error)]
#[error("Connection to {host}:{port} failed after {attempts} attempts")]
pub struct ConnectionError {
pub host: String,
pub port: u16,
pub attempts: u32,
#[source]
pub source: std::io::Error,
}
#[derive(Debug, Error)]
pub enum NetworkError {
#[error("Timeout after {duration:?}")]
Timeout { duration: Duration },
#[error("DNS resolution failed for '{hostname}'")]
DnsResolution { hostname: String },
#[error(transparent)]
Connection(#[from] ConnectionError),
#[error(transparent)]
Io(#[from] std::io::Error),
}
fn connect(host: &str, port: u16) -> Result<(), NetworkError> {
// Simulate connection
Err(NetworkError::DnsResolution {
hostname: host.to_string()
})
}
fn main() {
match connect("example.com", 443) {
Ok(()) => println!("Connected!"),
Err(e) => println!("Error: {}", e),
}
}use std::io;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AppError {
#[error("Application configuration error")]
Config,
#[error("Database error")]
Database(#[source] sql::Error),
// Transparent forwarding - preserves the original error message
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
// Mock sql module
mod sql {
use std::fmt;
#[derive(Debug)]
pub struct Error(pub String);
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SQL error: {}", self.0)
}
}
impl std::error::Error for Error {}
}
fn main() {
let err = AppError::Database(sql::Error("connection failed".to_string()));
println!("Error: {}", err);
// Transparent error preserves original message
let io_err = io::Error::new(io::ErrorKind::NotFound, "file missing");
let app_err: AppError = io_err.into();
println!("Transparent error: {}", app_err);
}use std::net::IpAddr;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum HttpError {
#[error("HTTP {method} to {uri} failed with status {status_code}")]
RequestFailed {
method: String,
uri: String,
status_code: u16,
},
#[error("Request to {uri} timed out after {timeout_ms}ms")]
Timeout {
uri: String,
timeout_ms: u64,
},
#[error("Invalid URL: {url}")]
InvalidUrl {
url: String,
#[source]
source: url::ParseError,
},
#[error("Rate limited. Retry after {retry_after_secs} seconds")]
RateLimited {
retry_after_secs: u64,
},
#[error("IP {ip} is blocked: {reason}")]
Blocked {
ip: IpAddr,
reason: String,
},
}
// Mock url module
mod url {
use std::fmt;
#[derive(Debug)]
pub struct ParseError(pub String);
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "URL parse error: {}", self.0)
}
}
impl std::error::Error for ParseError {}
}
fn make_request(url: &str) -> Result<String, HttpError> {
Err(HttpError::RateLimited { retry_after_secs: 60 })
}
fn main() {
match make_request("https://api.example.com/data") {
Ok(body) => println!("Response: {}", body),
Err(e) => println!("Error: {}", e),
}
}use std::io;
use thiserror::Error;
/// A result type alias for this library
pub type Result<T> = std::result::Result<T, Error>;
/// The main error type for this library
#[derive(Debug, Error)]
pub enum Error {
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Operation not allowed in current state")]
InvalidState,
#[error("Resource '{name}' exhausted")]
ResourceExhausted { name: String },
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Parse(#[from] std::num::ParseIntError),
}
pub fn parse_and_validate(input: &str) -> Result<i32> {
let n: i32 = input.parse()?;
if n < 0 {
return Err(Error::InvalidInput("value must be non-negative".into()));
}
if n > 1000 {
return Err(Error::ResourceExhausted { name: "counter".into() });
}
Ok(n)
}
pub fn process(input: &str) -> Result<i32> {
let n = parse_and_validate(input)?;
Ok(n * 2)
}
fn main() {
match process("-5") {
Ok(n) => println!("Result: {}", n),
Err(e) => eprintln!("Error: {}", e),
}
}#[derive(Error)] to automatically implement std::error::Error and Display#[error("format string {field}")] using struct fields or tuple indices#[source] attribute to preserve the original causeFrom implementations with #[from] for seamless ? operator use#[error(transparent)] to forward error display directly (preserves original message){0}, {1} etc. for positional arguments in error messages{field_name}#[source] can be combined with #[from] for both chaining and automatic conversionResult<T> type alias for ergonomic function signatures in librariesanyhow for application code using #[error(transparent)]source() method on errors allows traversing the error chain programmatically