BudiBadu Logo
Samplebadu

Rust by Example: Custom Error Types

Rust 1.75+

Take control of your application's error reporting by defining custom error types. This example shows how to implement the std::error::Error trait and use enums to represent domain-specific failure modes.

Code

use std::fmt;
use std::error::Error;

// Define a custom error type
#[derive(Debug)]
struct DivisionByZeroError;

// Implement Display for user-facing output
impl fmt::Display for DivisionByZeroError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Cannot divide by zero")
    }
}

// Implement the Error trait (requires Debug and Display)
impl Error for DivisionByZeroError {}

// A function that uses the custom error
fn divide(a: f64, b: f64) -> Result<f64, DivisionByZeroError> {
    if b == 0.0 {
        return Err(DivisionByZeroError);
    }
    Ok(a / b)
}

// --- More Complex Error Type ---
#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
    Custom(String),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::Io(err) => write!(f, "IO Error: {}", err),
            AppError::Parse(err) => write!(f, "Parse Error: {}", err),
            AppError::Custom(msg) => write!(f, "App Error: {}", msg),
        }
    }
}

// Implement Error trait
// We can optionally implement source() to enable error chaining
impl Error for AppError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            AppError::Io(err) => Some(err),
            AppError::Parse(err) => Some(err),
            AppError::Custom(_) => None,
        }
    }
}

fn main() {
    match divide(10.0, 0.0) {
        Ok(result) => println!("Result: {}", result),
        Err(e) => println!("Error occurred: {}", e),
    }
}

Explanation

Robust applications require error types that accurately reflect their domain. While you can use simple strings, implementing the std::error::Error trait is the standard way to create interoperable errors. This trait requires your type to also implement Debug (for programmer logs) and Display (for user-facing messages).

A key feature of the Error trait is the source() method (formerly cause()). This allows for Error Chaining, where a high-level error (like AppError::Io) wraps a low-level error (like std::io::Error). Error reporting tools can traverse this chain to print a full backtrace or causal history.

For libraries, it is best practice to define a custom enum for your errors to allow consumers to match on specific failure modes. To reduce boilerplate, the Rust ecosystem heavily relies on the thiserror crate for libraries (which derives Error automatically) and the anyhow crate for applications (which provides a dynamic error type with easy context attachment).

Code Breakdown

5
#[derive(Debug)]. This attribute automatically generates the code needed to print the struct using {:?}. It is a strict requirement for implementing the Error trait.
9
impl fmt::Display. This trait controls how the type is printed with {}. We use the write! macro to format the error message into the provided formatter f.
47
fn source(&self). By returning the underlying error, we enable error reporting tools to "see through" our wrapper. For example, AppError::Io(err) returns Some(err), linking the chain.