Error Wrapping

When programming in Rust, there is a common need to not only raise new errors, but also attach additional details to an error that has previously been raised. This is mainly to allow a caller to attach additional details about which higher-level operations are being performed, so that better error report and diagnostics can be presented to the user.

Error libraries such as anyhow and eyre provide methods such as context and wrap_err to allow wrapping of additional details to their error type. In this chapter, we will discuss about how to implement context-generic error wrapping with CGP, and how to integrate them with existing error libraries.

Example: Config Loader

Supposed that we want to build an application with the functionality to load and parse some application configuration from a config path. Using the CGP patterns that we have learned so far, we may implement a context-generic config loader as follows:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate serde;
extern crate serde_json;

pub mod main {
pub mod traits {
    use std::path::PathBuf;

    use cgp::prelude::*;

    #[cgp_component {
        name: ConfigTypeComponent,
        provider: ProvideConfigType,
    }]
    pub trait HasConfigType {
        type Config;
    }

    #[cgp_component {
        provider: ConfigLoader,
    }]
    pub trait CanLoadConfig: HasConfigType + HasErrorType {
        fn load_config(&self) -> Result<Self::Config, Self::Error>;
    }

    #[cgp_component {
        provider: ConfigPathGetter,
    }]
    pub trait HasConfigPath {
        fn config_path(&self) -> &PathBuf;
    }
}

pub mod impls {
    use std::{fs, io};

    use cgp::core::error::{ErrorRaiser, ProvideErrorType};
    use cgp::prelude::*;
    use serde::Deserialize;

    use super::traits::*;

    pub struct LoadJsonConfig;

    impl<Context> ConfigLoader<Context> for LoadJsonConfig
    where
        Context: HasConfigType
            + HasConfigPath
            + CanRaiseError<io::Error>
            + CanRaiseError<serde_json::Error>,
        Context::Config: for<'a> Deserialize<'a>,
    {
        fn load_config(context: &Context) -> Result<Context::Config, Context::Error> {
            let config_path = context.config_path();

            let config_bytes = fs::read(config_path).map_err(Context::raise_error)?;

            let config = serde_json::from_slice(&config_bytes).map_err(Context::raise_error)?;

            Ok(config)
        }
    }
}
}
}

We first define the HasConfigType trait, which provides an abstract Config type to represent the application's config. We then define a CanLoadConfig trait, which provides an interface for loading the application config. To help with the implementation, we also implement a HasConfigPath trait, which allows a provider to get the file path to the config file from the context.

Using the config traits, we then implement LoadJsonConfig as a context-generic provider for ConfigLoader, which would read a JSON config file as bytes from the filesystem using std::fs::read, and then parse the config using serde_json. With CGP, LoadJsonConfig can work with any Config type that implements Deserialize.

We can then define an example application context that makes use of LoadJsonConfig to load its config as follows:

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate cgp;
extern crate serde;
extern crate serde_json;

pub mod main {
pub mod traits {
    use std::path::PathBuf;

    use cgp::prelude::*;

    #[cgp_component {
        name: ConfigTypeComponent,
        provider: ProvideConfigType,
    }]
    pub trait HasConfigType {
        type Config;
    }

    #[cgp_component {
        provider: ConfigLoader,
    }]
    pub trait CanLoadConfig: HasConfigType + HasErrorType {
        fn load_config(&self) -> Result<Self::Config, Self::Error>;
    }

    #[cgp_component {
        provider: ConfigPathGetter,
    }]
    pub trait HasConfigPath {
        fn config_path(&self) -> &PathBuf;
    }
}

pub mod impls {
    use std::{fs, io};

    use cgp::core::error::{ErrorRaiser, ProvideErrorType};
    use cgp::prelude::*;
    use serde::Deserialize;

    use super::traits::*;

    pub struct LoadJsonConfig;

    impl<Context> ConfigLoader<Context> for LoadJsonConfig
    where
        Context: HasConfigType
            + HasConfigPath
            + CanRaiseError<io::Error>
            + CanRaiseError<serde_json::Error>,
        Context::Config: for<'a> Deserialize<'a>,
    {
        fn load_config(context: &Context) -> Result<Context::Config, Context::Error> {
            let config_path = context.config_path();

            let config_bytes = fs::read(config_path).map_err(Context::raise_error)?;

            let config = serde_json::from_slice(&config_bytes).map_err(Context::raise_error)?;

            Ok(config)
        }
    }

    pub struct UseAnyhowError;

    impl<Context> ProvideErrorType<Context> for UseAnyhowError {
        type Error = anyhow::Error;
    }

    pub struct RaiseFrom;

    impl<Context, SourceError> ErrorRaiser<Context, SourceError> for RaiseFrom
    where
        Context: HasErrorType,
        Context::Error: From<SourceError>,
    {
        fn raise_error(e: SourceError) -> Context::Error {
            e.into()
        }
    }
}

pub mod contexts {
    use std::io;
    use std::path::PathBuf;

    use cgp::core::component::UseDelegate;
    use cgp::core::error::{ErrorRaiserComponent, ErrorTypeComponent};
    use cgp::prelude::*;
    use serde::Deserialize;

    use super::impls::*;
    use super::traits::*;

    pub struct App {
        pub config_path: PathBuf,
    }

    #[derive(Deserialize)]
    pub struct AppConfig {
        pub api_secret: String,
    }

    pub struct AppComponents;

    pub struct RaiseAppErrors;

    impl HasComponents for App {
        type Components = AppComponents;
    }

    delegate_components! {
        AppComponents {
            ErrorTypeComponent: UseAnyhowError,
            ErrorRaiserComponent: UseDelegate<RaiseAppErrors>,
            ConfigLoaderComponent: LoadJsonConfig,
        }
    }

    delegate_components! {
        RaiseAppErrors {
            [
                io::Error,
                serde_json::Error,
            ]:
                RaiseFrom,
        }
    }

    impl ProvideConfigType<App> for AppComponents {
        type Config = AppConfig;
    }

    impl ConfigPathGetter<App> for AppComponents {
        fn config_path(app: &App) -> &PathBuf {
            &app.config_path
        }
    }

    pub trait CanUseApp: CanLoadConfig {}

    impl CanUseApp for App {}
}
}
}

The App context has a config_path field to store the path to the JSON config. We also define an example AppConfig type, which implements Deserialize and has an api_secret string field that can be used by further implementation.

Inside the component wiring for AppComponents, we make use of UseAnyhowError that we have defined in earlier chapter to provide the anyhow::Error type, and we use UseDelegate<RaiseAppErrors> to implement the error raiser. Inside of RaiseAppErrors, we make use of RaiseFrom to convert std::io::Error and serde_json::Error to anyhow::Error using the From instance.

We also provide context-specific implementations of ProvideConfigType and ConfigPathGetter for the App context. Following that, we define a check trait CanUseApp to check that the wiring is done correctly and that App implements CanLoadConfig.

Even though the example implementation for LoadJsonConfig works, we would quickly find out that the error message returned from it is not very helpful. For example, if the file does not exist, we would get the following error message:

No such file or directory (os error 2)

Similarly, if the config file is not in JSON format, we would get an error message like the following:

expected value at line 1 column 2

Error messages like above make it very difficult for users to figure out what went wrong, and what action needs to be taken to resolve them. To improve the error messages, we need to wrap around source errors like std::io::Error, and provide additional details so that the user knows that the error occured when trying to load the app config. Next, we will learn about how to wrap around these errors in CGP.

Error Wrapper

With the same motivation described in the previous chapter, we would like to make use of CGP to also enable modular error reporting for the error details that is being wrapped. This would mean that we want to define a generic Detail type that can include structured data inside the error details. We can do that by introduce an error wrapper trait as follows:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::prelude::*;

#[cgp_component {
    provider: ErrorWrapper,
}]
pub trait CanWrapError<Detail>: HasErrorType {
    fn wrap_error(error: Self::Error, detail: Detail) -> Self::Error;
}
}

The CanWrapError trait is parameterized by a generic Detail type, and has HasErrorType as its supertrait. Inside the wrap_error method, it first accepts a context error Self::Error and also a Detail value. It then wraps the detail inside the context error, and return Self::Error.

To see how CanWrapError works in practice, we can redefine LoadJsonConfig to use CanWrapError as follows:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate serde;
extern crate serde_json;

use std::path::PathBuf;
use core::fmt::Display;
use std::{fs, io};

use cgp::prelude::*;
use serde::Deserialize;

#[cgp_component {
    name: ConfigTypeComponent,
    provider: ProvideConfigType,
}]
pub trait HasConfigType {
    type Config;
}

#[cgp_component {
    provider: ConfigLoader,
}]
pub trait CanLoadConfig: HasConfigType + HasErrorType {
    fn load_config(&self) -> Result<Self::Config, Self::Error>;
}

#[cgp_component {
    provider: ConfigPathGetter,
}]
pub trait HasConfigPath {
    fn config_path(&self) -> &PathBuf;
}

pub struct LoadJsonConfig;

impl<Context> ConfigLoader<Context> for LoadJsonConfig
where
    Context: HasConfigType
        + HasConfigPath
        + CanWrapError<String>
        + CanRaiseError<io::Error>
        + CanRaiseError<serde_json::Error>,
    Context::Config: for<'a> Deserialize<'a>,
{
    fn load_config(context: &Context) -> Result<Context::Config, Context::Error> {
        let config_path = context.config_path();

        let config_bytes = fs::read(config_path).map_err(|e| {
            Context::wrap_error(
                Context::raise_error(e),
                format!(
                    "error when reading config file at path {}",
                    config_path.display()
                ),
            )
        })?;

        let config = serde_json::from_slice(&config_bytes).map_err(|e| {
            Context::wrap_error(
                Context::raise_error(e),
                format!(
                    "error when parsing JSON config file at path {}",
                    config_path.display()
                ),
            )
        })?;

        Ok(config)
    }
}
}

Inside the new implementation of LoadJsonConfig, we add a CanWrapError<String> constraint so that we can add stringly error details inside the provider. When mapping the errors returned from std::fs::read and serde_json::from_slice, we pass in a closure instead of directly calling Context::raise_error. Since the first argument of wrap_error expects a Context::Error, we would still first use Context::raise_error to raise std::io::Error and serde_json::Error into Context::Error. In the second argument, we use format! to add additional details that the errors occured when we are trying to read and parse the given config file.

By looking only at the example, it may seem redundant that we have to first raise a concrete source error like std::io::Error into Context::Error, before wrapping it again using Context::wrap_error. If the reader prefers, you can also use a constraint like CanRaiseError<(String, std::io::Error)> to raise the I/O error with additional string detail.

However, the interface for CanWrapError is more applicable generally, especially when we combine the use with other abstractions. For example, we may want to define a trait like CanReadFile to try reading a file, and returning a general Context::Error when the read fails. In that case, we can still use wrap_error without knowing about whether we are dealing with concrete errors or abstract errors.

Next, we would need to implement a provider for CanWrapError to handle how to wrap additional details into the error value. In the case when the context error type is anyhow::Error, we can simply call the context method. So we can implement an error wrapper provider for anyhow::Error as follows:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

use core::fmt::Display;

use cgp::prelude::*;
use cgp::core::error::ErrorWrapper;

pub struct WrapWithAnyhowContext;

impl<Context, Detail> ErrorWrapper<Context, Detail> for WrapWithAnyhowContext
where
    Context: HasErrorType<Error = anyhow::Error>,
    Detail: Display + Send + Sync + 'static,
{
    fn wrap_error(error: anyhow::Error, detail: Detail) -> anyhow::Error {
        error.context(detail)
    }
}
}

We implement WrapWithAnyhowContext as a context-generic provider for anyhow::Error. It is implemented for any context type Context with Context::Error being the same as anyhow::Error. Additionally, it is implemented for any Detail type that implements Display + Send + Sync + 'static, as those are the required trait bounds to use anyhow::Error::context. Inside the wrap_error implementation, we simply call error.context(detail) to wrap the error detail using anyhow.

After rewiring the application with the new providers, if we run the application again with missing file, it would show the following error instead:

error when reading config file at path config.json

Caused by:
    No such file or directory (os error 2)

Similarly, when encountering error parsing the config JSON, the application now shows the error message:

error when parsing JSON config file at path config.toml

Caused by:
    expected value at line 1 column 2

As we can see, the error messages are now much more informative, allowing the user to diagnose what went wrong and fix the problem.

Structured Error Wrapping

Similar to the reasons for using structured error reporting from the previous chapter, using structured error details would make it possible to decouple how to format the wrapped error detail from the provider. For the case of LoadJsonConfig, we can define and use a structured error detail type as follows:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate serde;
extern crate serde_json;

use std::path::PathBuf;
use core::fmt::Debug;
use std::{fs, io};

use cgp::prelude::*;
use serde::Deserialize;

#[cgp_component {
    name: ConfigTypeComponent,
    provider: ProvideConfigType,
}]
pub trait HasConfigType {
    type Config;
}

#[cgp_component {
    provider: ConfigLoader,
}]
pub trait CanLoadConfig: HasConfigType + HasErrorType {
    fn load_config(&self) -> Result<Self::Config, Self::Error>;
}

#[cgp_component {
    provider: ConfigPathGetter,
}]
pub trait HasConfigPath {
    fn config_path(&self) -> &PathBuf;
}

pub struct LoadJsonConfig;

pub struct ErrLoadJsonConfig<'a, Context> {
    pub context: &'a Context,
    pub config_path: &'a PathBuf,
    pub action: LoadJsonConfigAction,
}

pub enum LoadJsonConfigAction {
    ReadFile,
    ParseFile,
}

impl<Context> ConfigLoader<Context> for LoadJsonConfig
where
    Context: HasConfigType
        + HasConfigPath
        + CanRaiseError<io::Error>
        + CanRaiseError<serde_json::Error>
        + for<'a> CanWrapError<ErrLoadJsonConfig<'a, Context>>,
    Context::Config: for<'a> Deserialize<'a>,
{
    fn load_config(context: &Context) -> Result<Context::Config, Context::Error> {
        let config_path = context.config_path();

        let config_bytes = fs::read(config_path).map_err(|e| {
            Context::wrap_error(
                Context::raise_error(e),
                ErrLoadJsonConfig {
                    context,
                    config_path,
                    action: LoadJsonConfigAction::ReadFile,
                },
            )
        })?;

        let config = serde_json::from_slice(&config_bytes).map_err(|e| {
            Context::wrap_error(
                Context::raise_error(e),
                ErrLoadJsonConfig {
                    context,
                    config_path,
                    action: LoadJsonConfigAction::ParseFile,
                },
            )
        })?;

        Ok(config)
    }
}

impl<'a, Context> Debug for ErrLoadJsonConfig<'a, Context> {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self.action {
            LoadJsonConfigAction::ReadFile => {
                write!(
                    f,
                    "error when reading config file at path {}",
                    self.config_path.display()
                )
            }
            LoadJsonConfigAction::ParseFile => {
                write!(
                    f,
                    "error when parsing JSON config file at path {}",
                    self.config_path.display()
                )
            }
        }
    }
}
}

We first define an error detail struct ErrLoadJsonConfig that is parameterized by a lifetime 'a and a context type Context. Inside the struct, we include the Context field to allow potential extra details to be included from the concrete context. We also include the config_path to show the path of the config file that cause the error. Lastly, we also include a LoadJsonConfigAction field to indicate whether the error happened when reading or parsing the config file.

We also implement a Debug instance for ErrLoadJsonConfig, so that it can be used by default when there is no need to customize the display of the error detail. The Debug implementation ignores the context field, and shows the same error messages as we did before.

To make use of the Debug implementation with anyhow, we can implement a separate provider that wraps any Detail type that implements Debug as follows:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

use core::fmt::Debug;

use cgp::prelude::*;
use cgp::core::error::ErrorWrapper;

pub struct WrapWithAnyhowDebug;

impl<Context, Detail> ErrorWrapper<Context, Detail> for WrapWithAnyhowDebug
where
    Context: HasErrorType<Error = anyhow::Error>,
    Detail: Debug,
{
    fn wrap_error(error: anyhow::Error, detail: Detail) -> anyhow::Error {
        error.context(format!("{detail:?}"))
    }
}
}

To wrap the error, we first use Debug to format the error detail into string, and then call error.context with the string.

Full Example

With everything that we have learned so far, we can rewrite the config loader example in the beginning of this chapter, and make use of CanWrapError to decouple the error wrapping details from the provider LoadJsonConfig:

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate cgp;
extern crate serde;
extern crate serde_json;

pub mod main {
pub mod traits {
    use std::path::PathBuf;

    use cgp::core::component::UseDelegate;
    use cgp::prelude::*;

    #[cgp_component {
        name: ConfigTypeComponent,
        provider: ProvideConfigType,
    }]
    pub trait HasConfigType {
        type Config;
    }

    #[cgp_component {
        provider: ConfigLoader,
    }]
    pub trait CanLoadConfig: HasConfigType + HasErrorType {
        fn load_config(&self) -> Result<Self::Config, Self::Error>;
    }

    #[cgp_component {
        provider: ConfigPathGetter,
    }]
    pub trait HasConfigPath {
        fn config_path(&self) -> &PathBuf;
    }
}

pub mod impls {
    use core::fmt::{Debug, Display};
    use std::path::PathBuf;
    use std::{fs, io};

    use cgp::core::error::{ErrorRaiser, ErrorWrapper,ProvideErrorType};
    use cgp::prelude::*;
    use serde::Deserialize;

    use super::traits::*;

    pub struct LoadJsonConfig;

    pub struct ErrLoadJsonConfig<'a, Context> {
        pub context: &'a Context,
        pub config_path: &'a PathBuf,
        pub action: LoadJsonConfigAction,
    }

    pub enum LoadJsonConfigAction {
        ReadFile,
        ParseFile,
    }

    impl<Context> ConfigLoader<Context> for LoadJsonConfig
    where
        Context: HasConfigType
            + HasConfigPath
            + CanRaiseError<io::Error>
            + CanRaiseError<serde_json::Error>
            + for<'a> CanWrapError<ErrLoadJsonConfig<'a, Context>>,
        Context::Config: for<'a> Deserialize<'a>,
    {
        fn load_config(context: &Context) -> Result<Context::Config, Context::Error> {
            let config_path = context.config_path();

            let config_bytes = fs::read(config_path).map_err(|e| {
                Context::wrap_error(
                    Context::raise_error(e),
                    ErrLoadJsonConfig {
                        context,
                        config_path,
                        action: LoadJsonConfigAction::ReadFile,
                    },
                )
            })?;

            let config = serde_json::from_slice(&config_bytes).map_err(|e| {
                Context::wrap_error(
                    Context::raise_error(e),
                    ErrLoadJsonConfig {
                        context,
                        config_path,
                        action: LoadJsonConfigAction::ParseFile,
                    },
                )
            })?;

            Ok(config)
        }
    }

    impl<'a, Context> Debug for ErrLoadJsonConfig<'a, Context> {
        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
            match self.action {
                LoadJsonConfigAction::ReadFile => {
                    write!(
                        f,
                        "error when reading config file at path {}",
                        self.config_path.display()
                    )
                }
                LoadJsonConfigAction::ParseFile => {
                    write!(
                        f,
                        "error when parsing JSON config file at path {}",
                        self.config_path.display()
                    )
                }
            }
        }
    }

    pub struct UseAnyhowError;

    impl<Context> ProvideErrorType<Context> for UseAnyhowError {
        type Error = anyhow::Error;
    }

    pub struct RaiseFrom;

    impl<Context, SourceError> ErrorRaiser<Context, SourceError> for RaiseFrom
    where
        Context: HasErrorType,
        Context::Error: From<SourceError>,
    {
        fn raise_error(e: SourceError) -> Context::Error {
            e.into()
        }
    }

    pub struct WrapWithAnyhowDebug;

    impl<Context, Detail> ErrorWrapper<Context, Detail> for WrapWithAnyhowDebug
    where
        Context: HasErrorType<Error = anyhow::Error>,
        Detail: Debug,
    {
        fn wrap_error(error: anyhow::Error, detail: Detail) -> anyhow::Error {
            error.context(format!("{detail:?}"))
        }
    }
}

pub mod contexts {
    use std::io;
    use std::path::PathBuf;

    use cgp::core::component::UseDelegate;
    use cgp::core::error::{ErrorRaiserComponent, ErrorWrapperComponent, ErrorTypeComponent};
    use cgp::prelude::*;
    use serde::Deserialize;

    use super::impls::*;
    use super::traits::*;

    pub struct App {
        pub config_path: PathBuf,
    }

    #[derive(Deserialize)]
    pub struct AppConfig {
        pub secret: String,
    }

    pub struct AppComponents;

    pub struct RaiseAppErrors;

    impl HasComponents for App {
        type Components = AppComponents;
    }

    delegate_components! {
        AppComponents {
            ErrorTypeComponent: UseAnyhowError,
            ErrorRaiserComponent: UseDelegate<RaiseAppErrors>,
            ErrorWrapperComponent: WrapWithAnyhowDebug,
            ConfigLoaderComponent: LoadJsonConfig,
        }
    }

    delegate_components! {
        RaiseAppErrors {
            [
                io::Error,
                serde_json::Error,
            ]:
                RaiseFrom,
        }
    }

    impl ProvideConfigType<App> for AppComponents {
        type Config = AppConfig;
    }

    impl ConfigPathGetter<App> for AppComponents {
        fn config_path(app: &App) -> &PathBuf {
            &app.config_path
        }
    }

    pub trait CanUseApp: CanLoadConfig {}

    impl CanUseApp for App {}
}
}
}

Delegated Error Wrapping

Similar to the previous chapter on delegated error raisers, we can also make use of the UseDelegate pattern to implement delegated error wrapping as follows:

#![allow(unused)]
fn main() {
extern crate cgp;

use core::marker::PhantomData;

use cgp::prelude::*;
use cgp::core::error::ErrorWrapper;

pub struct UseDelegate<Components>(pub PhantomData<Components>);

impl<Context, Detail, Components> ErrorWrapper<Context, Detail> for UseDelegate<Components>
where
    Context: HasErrorType,
    Components: DelegateComponent<Detail>,
    Components::Delegate: ErrorWrapper<Context, Detail>,
{
    fn wrap_error(error: Context::Error, detail: Detail) -> Context::Error {
        Components::Delegate::wrap_error(error, detail)
    }
}
}

With this implementation, we can dispatch the handling of different error Detail type to different error wrappers, similar to how we dispatch the error raisers based on the SourceError type:

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate cgp;
extern crate serde;
extern crate serde_json;

pub mod main {
pub mod traits {
    use std::path::PathBuf;

    use cgp::core::component::UseDelegate;
    use cgp::core::error::ErrorWrapper;
    use cgp::prelude::*;

    #[cgp_component {
        name: ConfigTypeComponent,
        provider: ProvideConfigType,
    }]
    pub trait HasConfigType {
        type Config;
    }

    #[cgp_component {
        provider: ConfigLoader,
    }]
    pub trait CanLoadConfig: HasConfigType + HasErrorType {
        fn load_config(&self) -> Result<Self::Config, Self::Error>;
    }

    #[cgp_component {
        provider: ConfigPathGetter,
    }]
    pub trait HasConfigPath {
        fn config_path(&self) -> &PathBuf;
    }
}

pub mod impls {
    use core::fmt::{Debug, Display};
    use std::path::PathBuf;
    use std::{fs, io};

    use cgp::core::error::{ErrorRaiser, ErrorWrapper, ProvideErrorType};
    use cgp::prelude::*;
    use serde::Deserialize;

    use super::traits::*;

    pub struct LoadJsonConfig;

    pub struct ErrLoadJsonConfig<'a, Context> {
        pub context: &'a Context,
        pub config_path: &'a PathBuf,
        pub action: LoadJsonConfigAction,
    }

    pub enum LoadJsonConfigAction {
        ReadFile,
        ParseFile,
    }

    impl<Context> ConfigLoader<Context> for LoadJsonConfig
    where
        Context: HasConfigType
            + HasConfigPath
            + CanRaiseError<io::Error>
            + CanRaiseError<serde_json::Error>
            + for<'a> CanWrapError<ErrLoadJsonConfig<'a, Context>>,
        Context::Config: for<'a> Deserialize<'a>,
    {
        fn load_config(context: &Context) -> Result<Context::Config, Context::Error> {
            let config_path = context.config_path();

            let config_bytes = fs::read(config_path).map_err(|e| {
                Context::wrap_error(
                    Context::raise_error(e),
                    ErrLoadJsonConfig {
                        context,
                        config_path,
                        action: LoadJsonConfigAction::ReadFile,
                    },
                )
            })?;

            let config = serde_json::from_slice(&config_bytes).map_err(|e| {
                Context::wrap_error(
                    Context::raise_error(e),
                    ErrLoadJsonConfig {
                        context,
                        config_path,
                        action: LoadJsonConfigAction::ParseFile,
                    },
                )
            })?;

            Ok(config)
        }
    }

    impl<'a, Context> Debug for ErrLoadJsonConfig<'a, Context> {
        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
            match self.action {
                LoadJsonConfigAction::ReadFile => {
                    write!(
                        f,
                        "error when reading config file at path {}",
                        self.config_path.display()
                    )
                }
                LoadJsonConfigAction::ParseFile => {
                    write!(
                        f,
                        "error when parsing JSON config file at path {}",
                        self.config_path.display()
                    )
                }
            }
        }
    }

    pub struct UseAnyhowError;

    impl<Context> ProvideErrorType<Context> for UseAnyhowError {
        type Error = anyhow::Error;
    }

    pub struct RaiseFrom;

    impl<Context, SourceError> ErrorRaiser<Context, SourceError> for RaiseFrom
    where
        Context: HasErrorType,
        Context::Error: From<SourceError>,
    {
        fn raise_error(e: SourceError) -> Context::Error {
            e.into()
        }
    }

    pub struct WrapWithAnyhowContext;

    impl<Context, Detail> ErrorWrapper<Context, Detail> for WrapWithAnyhowContext
    where
        Context: HasErrorType<Error = anyhow::Error>,
        Detail: Display + Send + Sync + 'static,
    {
        fn wrap_error(error: anyhow::Error, detail: Detail) -> anyhow::Error {
            error.context(detail)
        }
    }

    pub struct WrapWithAnyhowDebug;

    impl<Context, Detail> ErrorWrapper<Context, Detail> for WrapWithAnyhowDebug
    where
        Context: HasErrorType<Error = anyhow::Error>,
        Detail: Debug,
    {
        fn wrap_error(error: anyhow::Error, detail: Detail) -> anyhow::Error {
            error.context(format!("{detail:?}"))
        }
    }
}

pub mod contexts {
    use std::io;
    use std::path::PathBuf;

    use cgp::core::component::UseDelegate;
    use cgp::core::error::{ErrorRaiserComponent, ErrorWrapperComponent, ErrorTypeComponent};
    use cgp::prelude::*;
    use serde::Deserialize;

    use super::impls::*;
    use super::traits::*;

pub struct App {
    pub config_path: PathBuf,
}

#[derive(Deserialize)]
pub struct AppConfig {
    pub secret: String,
}

pub struct AppComponents;

pub struct RaiseAppErrors;

pub struct WrapAppErrors;

impl HasComponents for App {
    type Components = AppComponents;
}

delegate_components! {
    AppComponents {
        ErrorTypeComponent: UseAnyhowError,
        ErrorRaiserComponent: UseDelegate<RaiseAppErrors>,
        ErrorWrapperComponent: UseDelegate<WrapAppErrors>,
        ConfigLoaderComponent: LoadJsonConfig,
    }
}

delegate_components! {
    RaiseAppErrors {
        [
            io::Error,
            serde_json::Error,
        ]:
            RaiseFrom,
    }
}

delegate_components! {
    WrapAppErrors {
        String: WrapWithAnyhowContext,
        <'a, Context> ErrLoadJsonConfig<'a, Context>:
            WrapWithAnyhowDebug,
        // add other error wrappers here
    }
}

    impl ProvideConfigType<App> for AppComponents {
        type Config = AppConfig;
    }

    impl ConfigPathGetter<App> for AppComponents {
        fn config_path(app: &App) -> &PathBuf {
            &app.config_path
        }
    }

    pub trait CanUseApp: CanLoadConfig {}

    impl CanUseApp for App {}
}
}
}

The above example shows the addition of a new WrapAppErrors type, which we use with delegate_components! to map the handling of String detail to WrapWithAnyhowContext, and ErrLoadJsonConfig detail to WrapWithAnyhowDebug. Following the same pattern, we will be able to customize how exactly each error detail is wrapped, by updating the mapping for WrapAppErrors.

Conclusion

In this chapter, we learned about how to perform abstract error wrapping to wrap additional details to an abstract error. The pattern for using CanWrapError is very similar to the patterns that we have previously learned for CanRaiseError. So this is mostly a recap of the same patterns, and also show readers how you can expect the same CGP pattern to be applied in many different places.

Similar to the advice from the previous chapters, it could be overwhelming for beginners to try to use the full structured error wrapping patterns introduced in this chapter. As a result, we encourage readers to start with using only String as the error detail when wrapping errors inside practice applications.

The need for structured error wrapping typically would only arise in large-scale applications, or when one wants to publish CGP-based library crates for others to build modular applications. As such, you can always revisit this chapter at a later time, and refactor your providers to make use of structured error details when you really need them.