Structured Error Types
use thiserror::Error;
use std::path::PathBuf;
#[derive(Error, Debug)]
enum FileError {
#[error("File not found: {path}")]
NotFound { path: PathBuf },
#[error("Permission denied: {path}")]
PermissionDenied { path: PathBuf },
#[error("Invalid format in {path}: {message}")]
InvalidFormat { path: PathBuf, message: String },
#[error("I/O error: {source}")]
IoError {
#[from]
source: std::io::Error,
backtrace: std::backtrace::Backtrace,
},
}
fn read_config(path: PathBuf) -> Result<Config, FileError> {
if !path.exists() {
return Err(FileError::NotFound { path });
}
if !path.extension().map_or(false, |ext| ext == "json") {
return Err(FileError::InvalidFormat {
path,
message: "Expected JSON file".to_string(),
});
}
let content = std::fs::read_to_string(&path)?;
// Parse config...
Ok(Config { /* ... */ })
}
Error Context and Propagation
Adding context to errors makes them more useful for debugging and user feedback:
Using anyhow for Error Context
use anyhow::{Context, Result};
fn read_config_file(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read config file: {}", path))?;
let config: Config = serde_json::from_str(&content)
.with_context(|| format!("Failed to parse config file: {}", path))?;
Ok(config)
}
fn main() -> Result<()> {
let config = read_config_file("config.json")
.context("Application configuration error")?;
// Use config...
Ok(())
}
Custom Context with thiserror
use thiserror::Error;
#[derive(Error, Debug)]
enum ConfigError {
#[error("Failed to read config file: {path}")]
ReadError {
path: String,
#[source]
source: std::io::Error,
},
#[error("Failed to parse config file: {path}")]
ParseError {
path: String,
#[source]
source: serde_json::Error,
},
#[error("Missing required field: {field}")]
MissingField { field: String },
}
fn read_config_file(path: &str) -> Result<Config, ConfigError> {
let content = std::fs::read_to_string(path)
.map_err(|e| ConfigError::ReadError {
path: path.to_string(),
source: e,
})?;
let config: Config = serde_json::from_str(&content)
.map_err(|e| ConfigError::ParseError {
path: path.to_string(),
source: e,
})?;
if config.api_key.is_empty() {
return Err(ConfigError::MissingField {
field: "api_key".to_string(),
});
}
Ok(config)
}
Error Chain Pattern
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct AppError {
message: String,
source: Option<Box<dyn Error + Send + Sync>>,
}
impl AppError {
fn new<M: Into<String>>(message: M) -> Self {
AppError {
message: message.into(),
source: None,
}
}
fn with_source<M: Into<String>, E: Error + Send + Sync + 'static>(
message: M,
source: E,
) -> Self {
AppError {
message: message.into(),
source: Some(Box::new(source)),
}
}
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl Error for AppError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source.as_ref().map(|s| s.as_ref() as &(dyn Error + 'static))
}
}
// Using the error chain
fn process_data(path: &str) -> Result<Data, AppError> {
let content = std::fs::read_to_string(path)
.map_err(|e| AppError::with_source(
format!("Failed to read file: {}", path),
e,
))?;
let data: Data = serde_json::from_str(&content)
.map_err(|e| AppError::with_source(
format!("Failed to parse file: {}", path),
e,
))?;
Ok(data)
}
Error Reporting and Handling
How errors are reported and handled can significantly impact user experience:
User-Friendly Error Messages
use thiserror::Error;
#[derive(Error, Debug)]
enum UserError {
#[error("We couldn't find your account. Please check your credentials.")]
AccountNotFound,
#[error("Your password is incorrect. Please try again.")]
InvalidPassword,
#[error("Your account is locked. Please contact support.")]
AccountLocked,
#[error("An internal error occurred. Please try again later.")]
InternalError(#[from] InternalError),
}
#[derive(Error, Debug)]
enum InternalError {
#[error("Database error: {0}")]
Database(String),
#[error("Network error: {0}")]
Network(String),
}
fn login(username: &str, password: &str) -> Result<User, UserError> {
let user = find_user(username)
.ok_or(UserError::AccountNotFound)?;
if !verify_password(&user, password) {
return Err(UserError::InvalidPassword);
}
if user.is_locked {
return Err(UserError::AccountLocked);
}
Ok(user)
}
// Handling errors in the UI
fn handle_login(username: &str, password: &str) {
match login(username, password) {
Ok(user) => {
println!("Welcome, {}!", user.name);
}
Err(e) => {
println!("Error: {}", e);
// Log internal errors for debugging
if let UserError::InternalError(internal) = &e {
log::error!("Internal error: {:?}", internal);
}
}
}
}