Implementing std::error::Error properly
Custom error types implement the std::error::Error trait, providing structured, type-safe error handling. They're superior to strings or integers because they:
source()use std::error::Error;
use std::fmt;
#[derive(Debug)]
pub enum MyError {
IoError(std::io::Error),
ParseError(String),
Custom(String),
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MyError::IoError(e) => write!(f, "IO error: {}", e),
MyError::ParseError(msg) => write!(f, "Parse error: {}", msg),
MyError::Custom(msg) => write!(f, "{}", msg),
}
}
}
impl Error for MyError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
MyError::IoError(e) => Some(e),
_ => None,
}
}
}
use std::error::Error;
use std::fmt;
/// HTTP client error types
#[derive(Debug)]
pub enum HttpError {
/// Network-level error (DNS, connection, etc.)
Network {
url: String,
source: std::io::Error,
},
/// HTTP protocol error (bad response, etc.)
Protocol {
status_code: u16,
message: String,
},
/// Request timeout
Timeout {
url: String,
duration_ms: u64,
},
/// Invalid URL format
InvalidUrl {
url: String,
reason: String,
},
/// JSON parsing error
JsonParse {
source: serde_json::Error,
body: String,
},
/// Authentication failed
Unauthorized {
realm: Option<String>,
},
}
impl fmt::Display for HttpError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
HttpError::Network { url, source } => {
write!(f, "Network error connecting to '{}': {}", url, source)
}
HttpError::Protocol { status_code, message } => {
write!(f, "HTTP {} - {}", status_code, message)
}
HttpError::Timeout { url, duration_ms } => {
write!(f, "Request to '{}' timed out after {}ms", url, duration_ms)
}
HttpError::InvalidUrl { url, reason } => {
write!(f, "Invalid URL '{}': {}", url, reason)
}
HttpError::JsonParse { source, body } => {
write!(f, "JSON parse error: {}. Body: {}", source,
if body.len() > 100 { &body[..100] } else { body })
}
HttpError::Unauthorized { realm } => {
write!(f, "Authentication required{}",
realm.as_ref().map(|r| format!(" for realm '{}'", r))
.unwrap_or_default())
}
}
}
}
impl Error for HttpError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
HttpError::Network { source, .. } => Some(source),
HttpError::JsonParse { source, .. } => Some(source),
_ => None,
}
}
}
// Conversion from std::io::Error
impl From<std::io::Error> for HttpError {
fn from(error: std::io::Error) -> Self {
HttpError::Network {
url: "unknown".to_string(),
source: error,
}
}
}
// Usage in HTTP client
pub struct HttpClient;
impl HttpClient {
pub fn get(&self, url: &str) -> Result<String, HttpError> {
// Validate URL
if !url.starts_with("http://") && !url.starts_with("https://") {
return Err(HttpError::InvalidUrl {
url: url.to_string(),
reason: "URL must start with http:// or https://".to_string(),
});
}
// Simulate network request
let response_status = 404;
if response_status == 401 {
return Err(HttpError::Unauthorized { realm: Some("api".to_string()) });
}
if response_status >= 400 {
return Err(HttpError::Protocol {
status_code: response_status,
message: "Not Found".to_string(),
});
}
Ok("response body".to_string())
}
}
// Error handling example
fn http_example() {
let client = HttpClient;
match client.get("invalid-url") {
Ok(body) => println!("Success: {}", body),
Err(e) => {
eprintln!("Error: {}", e);
// Pattern match for specific handling
match e {
HttpError::InvalidUrl { .. } => {
eprintln!("Fix your URL format");
}
HttpError::Unauthorized { .. } => {
eprintln!("Please authenticate");
}
HttpError::Network { ref url, .. } => {
eprintln!("Network issue with {}, retrying...", url);
}
_ => {}
}
// Print error chain
let mut source = e.source();
while let Some(err) = source {
eprintln!(" Caused by: {}", err);
source = err.source();
}
}
}
}
use std::error::Error;
use std::fmt;
#[derive(Debug)]
pub enum DbError {
/// Connection failed
ConnectionFailed {
host: String,
port: u16,
reason: String,
},
/// Query execution error
QueryError {
query: String,
error_code: String,
message: String,
},
/// Constraint violation (unique, foreign key, etc.)
ConstraintViolation {
constraint: String,
table: String,
details: String,
},
/// Transaction deadlock
Deadlock {
query: String,
other_transaction: Option<String>,
},
/// Connection pool exhausted
PoolExhausted {
max_connections: usize,
timeout_ms: u64,
},
/// Serialization error (optimistic locking)
SerializationFailure {
table: String,
row_id: i64,
},
}
impl fmt::Display for DbError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
DbError::ConnectionFailed { host, port, reason } => {
write!(f, "Failed to connect to {}:{} - {}", host, port, reason)
}
DbError::QueryError { query, error_code, message } => {
write!(f, "Query failed [{}]: {} - Query: {}",
error_code, message,
if query.len() > 50 { &query[..50] } else { query })
}
DbError::ConstraintViolation { constraint, table, details } => {
write!(f, "Constraint '{}' violated on table '{}': {}",
constraint, table, details)
}
DbError::Deadlock { query, other_transaction } => {
write!(f, "Deadlock detected while executing: {}{}",
query,
other_transaction.as_ref()
.map(|tx| format!(" (conflicting with: {})", tx))
.unwrap_or_default())
}
DbError::PoolExhausted { max_connections, timeout_ms } => {
write!(f, "Connection pool exhausted ({} connections, timeout after {}ms)",
max_connections, timeout_ms)
}
DbError::SerializationFailure { table, row_id } => {
write!(f, "Serialization failure on table '{}', row {}. Retry transaction.",
table, row_id)
}
}
}
}
impl Error for DbError {}
// Helper methods for error categorization
impl DbError {
/// Should this error trigger a retry?
pub fn is_retryable(&self) -> bool {
matches!(self,
DbError::Deadlock { .. } |
DbError::SerializationFailure { .. } |
DbError::PoolExhausted { .. }
)
}
/// Is this a client error (user's fault)?
pub fn is_client_error(&self) -> bool {
matches!(self,
DbError::ConstraintViolation { .. } |
DbError::QueryError { .. }
)
}
/// Get error severity for logging
pub fn severity(&self) -> &'static str {
match self {
DbError::ConnectionFailed { .. } => "CRITICAL",
DbError::Deadlock { .. } => "WARNING",
DbError::ConstraintViolation { .. } => "INFO",
_ => "ERROR",
}
}
}
// Usage with retry logic
fn db_example() {
let result = execute_with_retry(|| {
// Simulate database operation
Err(DbError::Deadlock {
query: "UPDATE accounts SET balance = ...".to_string(),
other_transaction: Some("tx_123".to_string()),
})
}, 3);
match result {
Ok(_) => println!("Success"),
Err(e) => {
eprintln!("[{}] {}", e.severity(), e);
if e.is_retryable() {
eprintln!("This error is retryable. Please retry your operation.");
}
}
}
}
fn execute_with_retry<F, T>(mut f: F, max_retries: usize) -> Result<T, DbError>
where
F: FnMut() -> Result<T, DbError>,
{
for attempt in 0..max_retries {
match f() {
Ok(result) => return Ok(result),
Err(e) if e.is_retryable() && attempt < max_retries - 1 => {
eprintln!("Attempt {} failed with retriable error, retrying...", attempt + 1);
std::thread::sleep(std::time::Duration::from_millis(100 * 2_u64.pow(attempt as u32)));
continue;
}
Err(e) => return Err(e),
}
}
unreachable!()
}
use std::error::Error;
use std::fmt;
use std::path::PathBuf;
#[derive(Debug)]
pub enum ConfigError {
/// Config file not found
FileNotFound {
path: PathBuf,
searched_paths: Vec<PathBuf>,
},
/// IO error reading file
IoError {
path: PathBuf,
source: std::io::Error,
},
/// Parse error (TOML, JSON, YAML, etc.)
ParseError {
path: PathBuf,
line: Option<usize>,
column: Option<usize>,
message: String,
},
/// Missing required field
MissingField {
field_name: String,
section: Option<String>,
},
/// Invalid value for field
InvalidValue {
field_name: String,
value: String,
expected: String,
},
/// Environment variable not set
MissingEnvVar {
var_name: String,
field_name: String,
},
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ConfigError::FileNotFound { path, searched_paths } => {
write!(f, "Config file not found: {:?}\nSearched in: {:?}",
path, searched_paths)
}
ConfigError::IoError { path, source } => {
write!(f, "Failed to read config from {:?}: {}", path, source)
}
ConfigError::ParseError { path, line, column, message } => {
write!(f, "Parse error in {:?}", path)?;
if let Some(line) = line {
write!(f, " at line {}", line)?;
if let Some(col) = column {
write!(f, ", column {}", col)?;
}
}
write!(f, ": {}", message)
}
ConfigError::MissingField { field_name, section } => {
write!(f, "Required field '{}' is missing{}",
field_name,
section.as_ref()
.map(|s| format!(" in section '{}'", s))
.unwrap_or_default())
}
ConfigError::InvalidValue { field_name, value, expected } => {
write!(f, "Invalid value '{}' for field '{}'. Expected: {}",
value, field_name, expected)
}
ConfigError::MissingEnvVar { var_name, field_name } => {
write!(f, "Environment variable '{}' required for field '{}' is not set",
var_name, field_name)
}
}
}
}
impl Error for ConfigError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ConfigError::IoError { source, .. } => Some(source),
_ => None,
}
}
}
impl From<std::io::Error> for ConfigError {
fn from(error: std::io::Error) -> Self {
ConfigError::IoError {
path: PathBuf::from("unknown"),
source: error,
}
}
}
// Helper for better error messages
impl ConfigError {
pub fn with_path(mut self, path: PathBuf) -> Self {
match &mut self {
ConfigError::IoError { path: p, .. } => *p = path,
ConfigError::ParseError { path: p, .. } => *p = path,
_ => {}
}
self
}
pub fn help_text(&self) -> Option<&'static str> {
match self {
ConfigError::FileNotFound { .. } => {
Some("Create a config file or specify --config flag")
}
ConfigError::MissingEnvVar { .. } => {
Some("Set the environment variable or add it to .env file")
}
ConfigError::InvalidValue { .. } => {
Some("Check the documentation for valid values")
}
_ => None,
}
}
}
#[derive(Debug)]
pub enum Error {
Io(std::io::Error),
Parse(String),
NotFound,
}
Pros: Simple, easy to use
Cons: Limited context, hard to extend
#[derive(Debug)]
pub enum Error {
Network { url: String, status: u16 },
Timeout { duration_ms: u64 },
}
Pros: Rich context, type-safe
Cons: More verbose
#[derive(Debug)]
pub enum Error {
Io(IoError),
Parse(ParseError),
}
#[derive(Debug)]
pub enum IoError {
FileNotFound(String),
PermissionDenied(String),
}
Pros: Organized, specific handling
Cons: Deep nesting complexity
// BAD: Loses type information
fn bad() -> Result<(), String> {
Err("something failed".to_string())
}
// GOOD: Structured error
fn good() -> Result<(), MyError> {
Err(MyError::SomethingFailed { details: "...".into() })
}
// BAD: Overly specific
pub enum Error {
FileNotFoundOnMonday,
FileNotFoundOnTuesday,
// ... one for each day
}
// GOOD: Single variant with data
pub enum Error {
FileNotFound { day: String },
}
// BAD: Just Debug output
impl Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self) // ❌ Not user-friendly
}
}
// GOOD: Human-readable message
impl Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MyError::NotFound { id } => {
write!(f, "Resource with ID {} not found", id)
}
// ...
}
}
}
Design error types for a REST API client with auth, rate limiting, and retries.
Hints:Create errors for a JSON/TOML parser with line/column info.
Hints:Build errors for a database transaction system.
Hints:HTTP client with comprehensive error types.
View on GitHubDatabase errors with SQL context.
View on GitHubJSON parsing errors with position info.
View on GitHubRun this code in the official Rust Playground