Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Idiomatic error propagation #411

Open
nwalfield opened this issue Jan 16, 2025 · 0 comments
Open

Idiomatic error propagation #411

nwalfield opened this issue Jan 16, 2025 · 0 comments

Comments

@nwalfield
Copy link

Currently, we have a number of crates where we have a top-level Error enum, but our functions return anyhow::Error instead of the enum. It's been pointed out that this is not idiomatic and it would be better to just return Error and use thiserror's transparent forwarding. I'm confused about how to idiomatically handle matching on forwarded error variants. Consider the following crate structure:

            high-level
       /                       \
low-level-a   low-level-b

where high-level defines Error, low-level-a defines ErrorA and low-level-b defines ErrorB. If each of the enums includes an IoError(std::io::Error) variant, how is code supposed to match on the std::io::Error? To make the question concrete, consider this code, which is what I currently imagine:

#[derive(thiserror::Error, Debug)]
enum Error {
    #[error("something bad happened: {0}")]
    Bad(String),

    #[error(transparent)]
    IoError(#[from] std::io::Error),
    #[error(transparent)]
    A(ErrorA),
    #[error(transparent)]
    B(ErrorB),
}

#[derive(thiserror::Error, Debug)]
enum ErrorA {
    #[error(transparent)]
    IoError(#[from] std::io::Error),
}

#[derive(thiserror::Error, Debug)]
enum ErrorB {
    #[error(transparent)]
    IoError(#[from] std::io::Error),
}

fn f() -> Result<(), Error> {
    Err(Error::Bad("ouch".into()))
}

fn main() {
    match f() {
        Ok(()) => {

        }
        Err(Error::IoError(err))
        | Err(Error::A(ErrorA::IoError(err)))
        | Err(Error::B(ErrorB::IoError(err))) => {
            // Handle the io error.
            eprintln!("io error: {}", err);
        }
        Err(err) => {
            eprintln!("An error occured: {}", err);
        }
    }
}

This is how we currently do it:

#[derive(thiserror::Error, Debug)]
enum Error {
    #[error("something bad happened: {0}")]
    Bad(String),
}

#[derive(thiserror::Error, Debug)]
enum ErrorA {
}

#[derive(thiserror::Error, Debug)]
enum ErrorB {
}

fn f() -> anyhow::Result<()> {
    Err(std::io::Error::new(std::io::ErrorKind::Other, "oh no!").into())
}

fn main() {
    let result = f();
    match result {
        Err(err) => {
            if let Some(err) = err.downcast_ref::<std::io::Error>() {
                // Handle the io error.
                eprintln!("io error: {}", err);
            } else {
                eprintln!("Not an io error: {}", err);
            }
        }
        Ok(()) => {
            eprintln!("Everything is fine")
        }
    }
}

That is, we downcast to std::io::Error and it doesn't matter if the std::io::Error comes from high-level, low-level-a, low-level-b, or another crate that high-level starts using later: the user of the high-level API can reliably and compactly catch std::io::Errors.

Thanks for any insights!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant