Course contentsShow
← Rust
Lesson 912 of 1,9107. Error Handling PatternsFree lesson

Generic Error Propagation

Design error wrappers that work polymorphically across disparate systems.

In this lesson, you will learn how to create flexible error wrappers that allow you to propagate errors across disparate subsystems without losing type information.

The Problem of Rigidity

When building large systems, you often integrate various crates: a database client, a network API, and a filesystem logger. Each has its own error type. If you force these into a single monolithic enum, you create a "God Error" that makes your code brittle. If one subsystem changes its error type, your entire application layer breaks.

The Generic Wrapper Pattern

Instead of hard-coding every possible error, you can design a generic wrapper that acts as a conduit for any error that satisfies the std::error::Error trait. This allows your middle-tier functions to remain agnostic about the specific implementation details of the underlying services.

Consider a "Gateway" service that handles requests but doesn't need to know if the underlying failure was a database timeout or a JSON parsing error:

use std::fmt::{Debug, Display};

/// A polymorphic wrapper that holds any error type
pub struct AppError<E> {
    pub message: String,
    pub source: E,
}

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

By keeping the internal source generic, you can propagate errors through your call stack while retaining the original context. When you hit a boundary—such as a public-facing API—you can then perform a final conversion, transforming the generic AppError<E> into a standard serialized response.

This approach decouples your business logic from the specific failure modes of your dependencies. You aren't just bubbling up errors; you are routing them through a uniform interface that remains valid even as your backend dependencies evolve independently. This pattern is particularly powerful when working with trait-based architecture, where a service might be implemented by different structs in development (mocked) versus production (real) environments.

Key Takeaway: Use generic error containers to decouple your propagation logic from specific subsystem types, ensuring your architecture remains resilient to dependency evolution.