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

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

Closed
genbattle opened this issue Jan 8, 2021 · 3 comments
Closed

Comments

@genbattle
Copy link

genbattle commented Jan 8, 2021

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
Copy link

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

@dtolnay
Copy link
Owner

dtolnay commented Feb 21, 2022

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.

@dtolnay dtolnay closed this as completed Feb 21, 2022
@amitu
Copy link

amitu commented May 16, 2024

@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?

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

4 participants