Web Analytics

Custom Errors

Intermediate ~35 min read

While Rust's standard library provides many error types, creating your own custom error types gives you better control over error messages, allows you to add context, and makes your error handling more structured and maintainable. Custom errors are essential for building robust, user-friendly applications.

Why Custom Errors?

Custom error types provide several benefits:

  • Better error messages: Provide context-specific error messages
  • Type safety: Compiler ensures you handle all error cases
  • Structured errors: Organize errors by domain or module
  • Error context: Include additional information about the error
  • Error chaining: Preserve error chain for debugging

Creating Custom Error Types

There are two main ways to create custom error types:

  1. Using enums: For multiple error variants
  2. Using structs: For errors with additional context

Custom Error with Enum

Enums are great when you have multiple error variants:

use std::fmt;

#[derive(Debug)]
enum MathError {
    DivisionByZero,
    NegativeNumber,
    Overflow,
}

impl fmt::Display for MathError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            MathError::DivisionByZero => write!(f, "Cannot divide by zero"),
            MathError::NegativeNumber => write!(f, "Number cannot be negative"),
            MathError::Overflow => write!(f, "Arithmetic overflow occurred"),
        }
    }
}

impl std::error::Error for MathError {}
Required Traits: To use your error type with Result and the ? operator, you need to implement both Display (for user-facing messages) and Error (for error handling).

Using Custom Error Types

Once you've defined your error type, you can use it in functions:

fn divide(a: i32, b: i32) -> Result<i32, MathError> {
    if b == 0 {
        Err(MathError::DivisionByZero)
    } else {
        Ok(a / b)
    }
}

fn sqrt(n: i32) -> Result<f64, MathError> {
    if n < 0 {
        Err(MathError::NegativeNumber)
    } else {
        Ok((n as f64).sqrt())
    }
}
Output
Click Run to execute your code

Custom Error with Struct

Structs are useful when you need to include additional context:

use std::fmt;

#[derive(Debug)]
struct ValidationError {
    field: String,
    message: String,
}

impl fmt::Display for ValidationError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Validation error in {}: {}", self.field, self.message)
    }
}

impl std::error::Error for ValidationError {}

fn validate_age(age: i32) -> Result<i32, ValidationError> {
    if age < 0 {
        Err(ValidationError {
            field: String::from("age"),
            message: String::from("Age cannot be negative"),
        })
    } else if age > 150 {
        Err(ValidationError {
            field: String::from("age"),
            message: String::from("Age cannot exceed 150"),
        })
    } else {
        Ok(age)
    }
}
Context Matters: Struct-based errors allow you to include context like which field failed, what value was provided, and why it failed. This makes debugging much easier!
Output
Click Run to execute your code

Implementing the Error Trait

The Error trait has several optional methods. The minimal implementation is just an empty impl block:

impl std::error::Error for MathError {}

However, you can implement additional methods for more functionality:

source() - Error Chaining

impl std::error::Error for AppError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.source.as_deref()
    }
}
Error Chaining: The source() method allows you to chain errors, preserving the original error while wrapping it with additional context. This is very useful for debugging.

Error Conversion

You can convert between error types using the From trait. This allows the ? operator to automatically convert errors:

impl From<std::num::ParseIntError> for MathError {
    fn from(err: std::num::ParseIntError) -> Self {
        MathError::ParseError(err.to_string())
    }
}
Automatic Conversion: Implementing From allows the ? operator to automatically convert between compatible error types, making error handling more flexible.

Best Practices

  • Use enums for variants: When you have multiple related error types
  • Use structs for context: When you need to include additional information
  • Implement Display: Always provide user-friendly error messages
  • Implement Error: Required for use with Result and ? operator
  • Add Debug derive: Makes debugging easier with {:?}
  • Use descriptive names: Make error types self-documenting
  • Include context: Add fields that help understand what went wrong
  • Implement From: For automatic error conversion

Common Mistakes

1. Forgetting to implement Display

// Wrong - No Display implementation
#[derive(Debug)]
enum MyError {
    SomethingWentWrong,
}

// Correct - Implement Display
#[derive(Debug)]
enum MyError {
    SomethingWentWrong,
}

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Something went wrong")
    }
}

2. Not implementing Error trait

// Wrong - Can't use with ? operator
#[derive(Debug)]
struct MyError {
    message: String,
}

impl fmt::Display for MyError { /* ... */ }

// Correct - Implement Error trait
impl std::error::Error for MyError {}

3. Generic error messages

// Wrong - Not helpful
impl fmt::Display for ValidationError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Error")  // Too generic!
    }
}

// Correct - Include context
impl fmt::Display for ValidationError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Validation error in {}: {}", self.field, self.message)
    }
}

4. Not using Debug derive

// Wrong - Can't use {:?}
enum MyError {
    SomethingWentWrong,
}

// Correct - Add Debug derive
#[derive(Debug)]
enum MyError {
    SomethingWentWrong,
}

Exercise: Custom Errors Practice

Task: Create custom error types and use them in your functions.

Requirements:

  • Create an error enum with multiple variants
  • Create an error struct with context fields
  • Implement Display and Error traits
  • Use your custom errors in functions
Output
Click Run to execute your code
Show Solution
use std::fmt;

#[derive(Debug)]
enum CalculationError {
    DivisionByZero,
    NegativeSquareRoot,
    Overflow,
}

impl fmt::Display for CalculationError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            CalculationError::DivisionByZero => write!(f, "Division by zero"),
            CalculationError::NegativeSquareRoot => write!(f, "Cannot take square root of negative number"),
            CalculationError::Overflow => write!(f, "Arithmetic overflow"),
        }
    }
}

impl std::error::Error for CalculationError {}

fn safe_calculate(a: f64, b: f64) -> Result<f64, CalculationError> {
    if b == 0.0 {
        return Err(CalculationError::DivisionByZero);
    }
    let result = a / b;
    if result < 0.0 {
        return Err(CalculationError::NegativeSquareRoot);
    }
    Ok(result.sqrt())
}

#[derive(Debug)]
struct ValidationError {
    field: String,
    value: String,
    message: String,
}

impl fmt::Display for ValidationError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Validation error in {} (value: '{}'): {}", 
               self.field, self.value, self.message)
    }
}

impl std::error::Error for ValidationError {}

fn validate_email(email: &str) -> Result<String, ValidationError> {
    if !email.contains('@') {
        Err(ValidationError {
            field: String::from("email"),
            value: email.to_string(),
            message: String::from("Email must contain '@'"),
        })
    } else {
        Ok(email.to_string())
    }
}

Summary

  • Custom errors provide better error messages and structure
  • Use enums for multiple error variants
  • Use structs for errors with context
  • Implement Display trait for user-friendly messages
  • Implement Error trait for use with Result
  • Add Debug derive for easier debugging
  • Implement From for automatic error conversion
  • Include context in error messages
  • Use descriptive error type names

What's Next?

Congratulations! You've completed the Error Handling module. You now understand how to handle errors in Rust using panics, Results, error propagation, and custom error types. These concepts are fundamental to writing robust Rust programs.

In the next modules, you'll learn about advanced Rust features like traits, generics, lifetimes, and more that build on these error handling foundations.