Course contentsShow
← Rust
Lesson 714 of 1,9106. Generics and Static DispatchFree lesson

Generic Error Handling Patterns

Design error propagation systems that wrap generic error types efficiently.

In this lesson, you will learn how to design flexible error-handling systems by leveraging generics to wrap diverse error types without sacrificing performance.

The Challenge of Heterogeneous Errors

When writing a library, you often deal with multiple underlying error types (like std::io::Error or std::num::ParseIntError). If you want to provide a unified custom error type for your library, using concrete types creates rigid code that is difficult to extend. Instead, we can use generics to create a "wrapper" error that can hold any type that satisfies the std::error::Error trait.

Implementing a Generic Wrapper

The idiomatic way to handle this is to create a struct that acts as a container for arbitrary error types, utilizing static dispatch to maintain speed.

use std::fmt;

#[derive(Debug)]
pub struct AppError<E> {
    pub message: String,
    pub source: E,
}

impl<E: fmt::Display> fmt::Display for AppError<E> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}: {}", self.message, self.source)
    }
}

By parameterizing AppError with <E>, you allow the compiler to generate a specific version of AppError for every distinct error type you pass into it. Because this happens at compile time, the compiler knows exactly which error type is inside, allowing it to inline calls and avoid the overhead associated with dynamic trait objects (like Box<dyn Error>).

Propagation with Traits

To propagate these generic errors, you can implement From for your wrapper. This allows you to use the ? operator seamlessly:

impl<E> From<E> for AppError<E> 
where E: std::error::Error {
    fn from(error: E) -> Self {
        AppError {
            message: "Operation failed".to_string(),
            source: error,
        }
    }
}

By defining the error structure generically, you decouple your error reporting logic from the specific source of the failure. This keeps your library's API clean while allowing your users to inject their own specific error types into your generic pipeline. This pattern ensures that your error handling remains as performant as hand-coded, specific error types while gaining the flexibility of abstraction.

Key Takeaway: Use generic structs to wrap diverse error types, ensuring your error-handling system remains performant through static dispatch while staying highly reusable across different contexts.