How do I define custom error types in Rust?

Walkthrough

Rust encourages handling errors explicitly through the Result type. While you can use Box<dyn Error> or string errors, defining your own error types leads to clearer APIs and better error handling. The thiserror crate simplifies this process with a derive macro.

Key concepts:

  1. Create an enum to represent different error cases
  2. Derive Error and Debug traits using thiserror
  3. Use #[error("...")} attributes to define error messages
  4. Use #[from] to automatically implement From for easy error conversion

This approach gives you type-safe errors that integrate seamlessly with Rust's ? operator while keeping your code clean and maintainable.

Code Example

# Cargo.toml
[dependencies]
thiserror = "1.0"
use std::io;
use thiserror::Error;
 
#[derive(Error, Debug)]
pub enum DataStoreError {
    #[error("Configuration file not found: {0}")
    ConfigMissing(String),
 
    #[error("Failed to read data: {0}")
    ReadError(#[from] io::Error),
 
    #[error("Invalid data format at line {line}: {message}")
    InvalidFormat { line: usize, message: String },
 
    #[error("Connection failed to {host}:{port}")
    ConnectionFailed { host: String, port: u16 },
 
    #[error("Unauthorized access for user '{username}'")
    Unauthorized { username: String },
}
 
// Example function that returns a custom error
fn parse_config(contents: &str) -> Result<Config, DataStoreError> {
    if contents.is_empty() {
        return Err(DataStoreError::ConfigMissing(
            "config file is empty".to_string()
        ));
    }
    
    // Parsing logic here...
    Ok(Config { /* ... */ })
}
 
// Example showing automatic conversion with #[from]
fn read_config_file(path: &str) -> Result<String, DataStoreError> {
    // io::Error automatically converts to DataStoreError::ReadError
    std::fs::read_to_string(path)?;
    Ok(String::new())
}
 
struct Config {
    // fields omitted
}
 
fn main() -> Result<(), DataStoreError> {
    // These errors display nicely with Display trait
    let err1 = DataStoreError::InvalidFormat {
        line: 42,
        message: "unexpected token".to_string(),
    };
    println!("Error: {}", err1);
    // Output: Error: Invalid data format at line 42: unexpected token
 
    let err2 = DataStoreError::ConnectionFailed {
        host: "localhost".to_string(),
        port: 5432,
    };
    println!("Error: {}", err2);
    // Output: Error: Connection failed to localhost:5432
 
    Ok(())
}

Summary

  • #[derive(Error)] generates the std::error::Error trait implementation
  • #[error("...")] defines the display message; use {0} for tuple variants or {field} for struct variants
  • #[from] on a field automatically implements From, enabling ? operator conversion
  • Custom error types provide type safety and clear, user-friendly error messages
  • Thiserror is a zero-cost abstraction—it compiles away and adds no runtime overhead