dtolnay / anyhow

Flexible concrete Error type built on std::error::Error
Apache License 2.0
5.51k stars 147 forks source link

Context returned by `Error::chain` can't be downcast to original type #135

Closed genbattle closed 2 years ago

genbattle commented 3 years ago

First of all thanks to the creators and maintainers of this library, it's been so instrumental in improving Rust's error handling story for me.

In my application I'm using anyhow::Error to create a stack of errors and information as an error "unwinds" through callers, then at the API level of my application I peel that back layer by layer using anyhow::Error::chain on the returned error. Some of the attached context is a typed error object which specifies more information about how the error should be translated at the API boundary (such as HTTP status codes) but I've found that the &dyn std::error::Error returned by chain can't be downcasted to the original type if it was attached to the error using anyhow::Error::context.

This failure only seems to be related to objects that are attached to the Error via the context method, and anyhow::Error::downcast_ref seems to correctly downcast to an object attached via context (it just only allows me to get the first instance of a class attached multiple times). This seems like a bug in how std::error::Error::downcast_ref interacts with the trait objects returned by chain, which I can only guess is related to the type erasure or wrapping going on within anyhow.

Here's a simplified example:

use std::error::Error;
use std::fmt;
use anyhow::anyhow;

struct MyError (
    u32
);

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl fmt::Debug for MyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "d: {}", self.0)
    }
}

impl Error for MyError {}

fn print_errors(err: &anyhow::Error) {
    err
        .chain()
        .for_each(|e| {
            if let Some(my_error) = e.downcast_ref::<MyError>() {
                println!("MyError {}", my_error);
            } else {
                println!("{}", e);
            }
        })
}

The following code:

    let e1 = anyhow!(MyError(1)).context(MyError(2));
    print_errors(&e1);

Produces:

2
MyError 1

Whereas I would expect it to produce:

MyError 2
MyError 1

So the original MyError object passed to anyhow! can be downcasted properly, but any MyError objects attached via context cannot be.

I'm guessing this may have something to do with the fact that context doesn't require arguments to impl std::error::Error, so there must be some sort of wrapper which is being used to return them from chain as an &dyn std::error::Error. I'm not sure if it was intended to support this use case or not, but it would be great if I could downcast context in the same way I can downcast the original error. It's frustrating that anyhow::Error::downcast_ref works on context correctly, but std::Error::downcast_ref on the references returned by chain doesn't.

When I did a quick scan of the issue tracker I thought this may be related to #84, but I think that although fixing this issue may also solve that one, they're fundamentally different approaches.

taylor1791 commented 2 years ago

I ran into this today in a similar use case. Would a PR be accepted to fix this issue?

dtolnay commented 2 years ago

I don't plan to experiment with alternative downcasting implementations as part of this crate so I am closing this issue, but I would be willing to consider a working PR if somebody sends one.

amitu commented 5 months ago

@genbattle I tried to do the same, I am also trying to see if I can attach HTTP status code to error. But its kind of not working as expected:

#[cfg(test)]
mod test {
    use anyhow::Context;

    #[derive(thiserror::Error, Debug)]
    enum EFirst {
        #[error("yo")]
        Yo,
    }

    fn outer() -> Result<(), anyhow::Error> {
        anyhow::Ok(out()?).context(http::StatusCode::CREATED)
    }

    fn out() -> Result<(), anyhow::Error> {
        anyhow::Ok(first()?).context(http::StatusCode::ACCEPTED)
    }

    fn first() -> Result<(), anyhow::Error> {
        Err(EFirst::Yo).context(http::StatusCode::SEE_OTHER)
    }

    #[test]
    fn t() {
        let e = outer().unwrap_err();
        assert_eq!(
            *e.downcast_ref::<http::StatusCode>().unwrap(),
            http::StatusCode::SEE_OTHER
        );
    }
}

I only get the error attached at first level when custom type was converted to anyhow::Error.

Trying to chain() with http::StatusCode doesn’t work as it is not Error. Then I tried:

    #[derive(thiserror::Error, Debug, PartialEq)]
    enum Status {
        #[error("created")]
        Created,
        #[error("accepted")]
        Accepted,
        #[error("see-other")]
        SeeOther,
    }

    fn outer2() -> Result<(), anyhow::Error> {
        anyhow::Ok(out2()?).context(Status::Created)
    }

    fn out2() -> Result<(), anyhow::Error> {
        anyhow::Ok(first2()?).context(Status::Accepted)
    }

    fn first2() -> Result<(), anyhow::Error> {
        Err(EFirst::Yo).context(Status::SeeOther)
    }

    #[test]
    fn t2() {
        let e = outer2().unwrap_err();
        println!("status: {:?}", e.downcast_ref::<Status>());
        for cause in e.chain() {
            println!("status: {:?}", cause.downcast_ref::<Status>());
        }
        assert!(false)
    }

Which gives:

---- error::test::t2 stdout ----
status: Some(SeeOther)
status: None
status: None
thread 'error::test::t2' panicked at ft-sdk/src/error.rs:105:9:

So the calls like anyhow::Ok(first2()?).context(Status::Accepted) have no effect.

@dtolnay in anyhow::Context: Effect on downcasting it says:

Some codebases prefer to use machine-readable context to categorize lower level errors in a way that will be actionable to higher levels of the application

Is this a bug, or I am I misunderstanding something?