Why can't the Option.expect() message be downcast as a &'static str when a panic is handled with catch_unwind?
Asked Answered
J

2

6

I have the following code:

use std::thread;
use std::panic;

pub fn main(){
    thread::spawn(move || {
        panic::catch_unwind(|| {
            // panic!("Oh no! A horrible error.");
            let s: Option<u32> = None;
            s.expect("Nothing was there!");
        })
    })
    .join()
    .and_then(|result| {
        match result {
            Ok(ref val) => {
                println!("No problems. Result was: {:?}", val);
            }
            Err(ref err) => {
                if let Some(err) = err.downcast_ref::<&'static str>() {
                    println!("Error: {}", err);
                } else {
                    println!("Unknown error type: {:?}", err);
                }
            }
        }
        result
    });
}

When I trigger a panic! directly (by uncommenting the line in the code above), then I get an output which includes my error message:

Error: Oh no! A horrible error.

But, if I use Option::expect(&str), as above, then the message cannot be downcast to &'static str, so I can't get the error message out:

Unknown error type: Any

How can I get the error message, and how would I find the correct type to downcast to in the general case?

Jobholder answered 25/2, 2017 at 15:56 Comment(0)
B
2

Option::expect expects a message as a &str, i.e. a string slice with any lifetime. You can't coerce a &str to a &'static str, as the string slice may refer to the interior of a String or Box<str> that could be freed at any time. If you were to keep a copy of the &'static str around, you would be able to use it after the String or Box<str> has been dropped, and that would be undefined behavior.

An importail detail is that the Any trait cannot hold any lifetime information (hence the 'static bound), as lifetimes in Rust are erased at compile time. Lifetimes are used by the compiler to validate your program, but a program cannot distinguish a &'a str from a &'b str from a &'static str at runtime.

[...] how would I find the correct type to downcast to in the general case?

Unfortunately, it's not easy. Any has a method (unstable as of Rust 1.15.1) named get_type_id that lets you obtain the TypeId of the concrete object referred to by the Any. That still doesn't tell you explicitly what type that is, as you still have to figure out which type this TypeId belongs to. You would have to get the TypeId of many types (using TypeId::of) and see if it matches the one you got from the Any, but you could do the same with downcast_ref.

In this instance, it turns out that the Any is a String. Perhaps Option::expect could eventually be specialized such that it panics with the string slice if its lifetime is 'static and only allocates a String if it's not 'static.

Bowing answered 25/2, 2017 at 17:38 Comment(1)
Thanks. I did some tests and found that even panic!(my_str) will end up being caught as a String when my_str is constructed at runtime. So the behaviour of panic!() is actually exactly what you described as your "perhaps eventually" behaviour for Option::expect.Jobholder
P
2

Like Francis said, you can't in general discover and cast to the type of a panic. However, that being said, panics have the following rules:

  • If you panic! with a single argument, the panic will have that type. Typically this is &'static str.
  • If you panic! with more than one argument, the arguments will be treated as format! parameters and used to create a String argument.

These rules are documented in the panic documentation: https://doc.rust-lang.org/std/panic/fn.catch_unwind.html.

With these rules in mind, we can write a function to extract the message from a panic in any case where there is a message available to be extracted, which in practice works most of the time, because most of the time the message is either &'static str or String:

pub fn get_panic_message(panic: &Box<dyn Any + Send>) -> Option<&str> {
    panic
        // Try to convert it to a String, then turn that into a str
        .downcast_ref::<String>()
        .map(String::as_str)
        // If that fails, try to turn it into a &'static str
        .or_else(|| panic.downcast_ref::<&'static str>().map(Deref::deref))
}

I use this exact function in an assertions library I wrote a while ago; you can see some examples of its use in the relevant test suite.

Pesthole answered 19/4, 2020 at 23:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.