Home/Error Handling Excellence/Custom Error Types

Custom Error Types

Implementing std::error::Error properly

intermediate
errorcustom-types
🎮 Interactive Playground

What are Custom Errors?

Custom error types implement the std::error::Error trait, providing structured, type-safe error handling. They're superior to strings or integers because they:

  • Preserve error context and causality
  • Enable pattern matching for handling
  • Support error chains with source()
  • Provide both user and developer messages
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,
        }
    }
}

Real-World Example 1: HTTP Client Errors (Network)

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();
            }
        }
    }
}

Real-World Example 2: Database Errors (Systems)

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!()
}

Real-World Example 3: Config Loading Errors (Systems)

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,
        }
    }
}

Error Type Design Patterns

Pattern 1: Flat Enum (Simple)

#[derive(Debug)]
pub enum Error {
    Io(std::io::Error),
    Parse(String),
    NotFound,
}
Pros: Simple, easy to use Cons: Limited context, hard to extend

Pattern 2: Structured Variants (Recommended)

#[derive(Debug)]
pub enum Error {
    Network { url: String, status: u16 },
    Timeout { duration_ms: u64 },
}
Pros: Rich context, type-safe Cons: More verbose

Pattern 3: Nested Errors

#[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

⚠️ Anti-Patterns

⚠️ ❌ Mistake #1: String Errors

// 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() })
}

⚠️ ❌ Mistake #2: Too Many Variants

// BAD: Overly specific
pub enum Error {
    FileNotFoundOnMonday,
    FileNotFoundOnTuesday,
    // ... one for each day
}

// GOOD: Single variant with data
pub enum Error {
    FileNotFound { day: String },
}

⚠️ ❌ Mistake #3: Implementing Display Badly

// 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)
            }
            // ...
        }
    }
}

Exercises

Exercise 1: API Client Errors

Design error types for a REST API client with auth, rate limiting, and retries.

Hints:
  • Network errors
  • Auth errors (401, 403)
  • Rate limit (429)
  • Retryable vs non-retryable

Exercise 2: Parser Errors

Create errors for a JSON/TOML parser with line/column info.

Hints:
  • Position tracking
  • Expected vs found
  • Helpful suggestions

Exercise 3: Transaction Errors

Build errors for a database transaction system.

Hints:
  • Deadlock
  • Constraint violations
  • Serialization failures
  • Retry logic helpers

Further Reading

Real-World Usage

🦀 reqwest

HTTP client with comprehensive error types.

View on GitHub

🦀 sqlx

Database errors with SQL context.

View on GitHub

🦀 serde_json

JSON parsing errors with position info.

View on GitHub

🎮 Try it Yourself

🎮

Custom Error Types - Playground

Run this code in the official Rust Playground