Mystified by downcast failure

I have some code that is trying to downcast Box<dyn std::error::Error> to my custom error type. It fails (downcast_ref() returns None), but the debug print of the error makes it look like it should have succeeded. I have tried simpler cases, but they all seem to work fine. I've spent quite a lot of time on this, and for the first time in many years I'm going to just give up and ask for help :frowning_face:.

https://github.com/BartMassey/parsere/blob/main/parsere/examples/skeltest.rs

Thanks much for any advice you can give.

I haven't fully diagnosed the problem, but a fix is to write

               captures.ok_or_else(|| {
                    Box::new(MyError::Mismatch {
                        re: RE,
                        txt: txt.to_string(),
                    }) as Box<dyn std::error::Error + 'static>
                })?

to make sure it gets unsized right after construction. I suspect that by putting it in a Result and then using the From impl, you get some kind of type wrapper which has the wrong type ID.

The problem is that the conversion from Box<MyError> to Box<dyn Error> happens by From conversion (from the ? syntax) instead of unsized coercion. If you write

captures.ok_or_else(|| {
    MyError::Mismatch {
        re: RE,
        txt: txt.to_string(),
    }
})?
// ^^^ TLDR: This is what you want to do!

or

captures.ok_or_else(|| {
    Box::new(MyError::Mismatch {
        re: RE,
        txt: txt.to_string(),
    }) as Box<dyn std::error::Error>
})?

then your test passes.

As it stands it uses this impl

impl<'a, E: Error + 'a> From<E> for Box<dyn Error + 'a>

which introduces another level of boxing. Note here, that

impl<T: Error> Error for Box<T>

is a thing, so the From<E> for Box<dyn Error + 'a> uses E == Box<MyError> in your case.

With the original code, the downcast would’ve only worked with downcast_ref::<Box<MyError>>(), e.g. like this:

fn main() {
    const TXT: &str = "50 ";
    let result: Result<Eg, Box<dyn Error + 'static>> = Eg::parse_re(TXT);
    match result {
        Err(e) => {
            match e.downcast_ref::<Box<MyError>>().map(std::ops::Deref::deref) {
                Some(&MyError::Mismatch { re, ref txt }) => {
                    assert_eq!(re, RE);
                    assert_eq!(txt, TXT);
                }
                Some(e) => panic!("wrong error: {:?}", e),
                None => panic!("downcast fail: {:?}", e),
            }
        }
        Ok(eg) => panic!("no error: {:?}", eg),
    }
}

Since Debug doesn’t print Boxes at all, your debug output is of course a bit confusing.

1 Like

Huh. Wouldn't have thought of that: thanks very very much!

Not that it matters so much, but I wonder if there's a bug to report against the From implementation here?

Edit: Looks like behaving as intended. Thanks for the other reply.

Enormous thanks! That makes things really clear.

Generally these sorts of things can't be changed because too much code relies on the exact way the current impls resolve. I don't even think edition boundaries are enough here because that doesn't apply to the library API.

Honestly, once @steffahn explained things fully, I'm convinced that this is expected behavior and not-a-bug.

Again, thanks to you both: really happy now.

As an aside, is there some macro that will show the full structure of its argument including boxes? I suspect that auto-deref normally makes this impossible (?) but I'd be fine with the macro taking the argument as owned in this sort of case.

I’m not aware of any way. But if you keep in mind that boxes (as well as e.g. arc IIRC, etc..) don’t show themselves in Debug output, it is not too bad. Actually, after you said that your downcast doesn’t work even though in your debug output it appears to be the right type, the first thing I thought about was that there might be some extra Box left. The rest was tracking how your code and the respective trait impls work. Usually/most often problems with Box vs no Box are caught by type checker, and Box<T> vs T is nothing that is supposed to have much of a semantical difference in Rust, so it does IMO make sense that these smart pointer types are usually invisible in Debug output which is mostly about the values of things.

Dynamic typing with Any or Error is kind-of the exception and a special thing. In this case, IMO more useful would be a way to pretty-print a TypeId (at least in debug builds) in a way that indicates what the type is that it represents. Right now, printing a TypeId is pretty unhelpful. Also a way to get the type_id out of a dyn Error would be needed, then you could just print the type you got in the case that downcast_ref fails.

1 Like

I thought about missing a Box, but for some reason I didn't think about an extra Box. Ah well. One of those bugs that I could have stared at for a week on my own, because I wasn't seeing what I wrote anymore.

It won't show the contents, but you can learn the structure with std::any::type_name. It recursively includes the names of all the type parameters (except lifetimes).