What does "join" return as an error on a panic?

When a thread panics, and another thread calls join_handle.join(), the parent thread gets back some kind of Err object. Trying to print that error value with println! debug syntax {:?} just prints Any { .. }. However, as can be seen in the Playground above, the same thing with anyhow! displays a backtrace as the contents of that object. So anyhow! knows how to format that.

What does .join() return as an error? Something that std::backtrace can process? Or the backtrace crate understands?

(Use case: GUI program with no console that needs to log errors and/or put them into a dialog box.)

The error returned is Box<dyn Any + Send + 'static> which contains the panic's argument. A string most of the time (slice or owned), but could be anything when panic_any is used. Since Any is a way of type erasure, you can get the original value only by explicit checks, e.g. downcasting. Box is obviously not an error, so there is no backtrace attached

The official documentation of the return type is quite good: Result in std::thread - Rust

The anyhow macro can take anything that implements Debug (or an error) and constructs a new anyhow error - with that message. What you're seeing is the stacktrace of the anyhow error, not the original panic from the other thread.

1 Like

As far as I can tell, the panic! macro returns either &'static str or String. However, when it returns one or the other is surprising. panic_any can return any value.

use std::thread::spawn;

fn main() {
    let err = spawn(|| panic!("oh no")).join().unwrap_err();
    println!("{:?}", err.downcast::<&'static str>().unwrap());

    let err = spawn(move || panic!("oh no {}", 5)).join().unwrap_err();
    println!("{:?}", err.downcast::<&'static str>().unwrap());

    let err = spawn(move || panic!("{}", 5)).join().unwrap_err();
    println!("{:?}", err.downcast::<String>().unwrap());

    let err = spawn(move || panic!("oh no {:?}", 5)).join().unwrap_err();
    println!("{:?}", err.downcast::<String>().unwrap());

    let x = 5;
    let err = spawn(move || panic!("oh no {x}")).join().unwrap_err();
    println!("{:?}", err.downcast::<String>().unwrap());

    let err = spawn(|| std::panic::panic_any(5)).join().unwrap_err();
    println!("{:?}", err.downcast::<i32>().unwrap());
}
1 Like

...wait, what? So panic is able to do compile-time formatting in some cases?

1 Like

Yeah, format_args!() gets optimized to a constant in some cases and panic!() tries to get an &'static str for the panic using args.as_str() and if that returns Some, it uses this as payload. args.as_str() is documented as having no guarantees about when it returns Some other than always returning Some when passing a literal string without any interpolation: https://doc.rust-lang.org/stable/std/fmt/struct.Arguments.html#method.as_str

3 Likes

What's puzzling me here is that the stack backtrace text was actually returned from "anyhow!". Note the output:

Err: "Error: Any { .. }

Stack backtrace:
...
15: main
  16: <unknown>
  17: __libc_start_main
  18: _start"

The backtrace is enclosed in "..." because that's coming from the programs' own println!() and I put those quotes in. It's not internal output to standard output.

That backtrace is useless, because it's a backtrace of the thread doing the join, not the one that panicked.

However,

it's possible to get the panic message to the joining thread, which is what I wanted.

Kind of weird, but usable.

1 Like

You will always see the backtrace printed to stderr in panicking threads with the RUST_BACKTRACE env var set (see std::panic::set_hook()). The backtrace includes the panic message, so you do not need to additionally print it after downcasting. In fact, you do not need to do anything other than set the env var.

If you need to capture the backtrace from a join handle and do something with it in code, that's a different matter.

FYI calling set_var will require unsafe in edition 2024. Guess they never got around to making it warn in the current edition...

you can manually capture a backtrace and use panic_any() to "throw" it:

use std::backtrace::Backtrace;
use std::panic::panic_any;

let join_handle = spawn(move || {
    panic_any(Backtrace::force_capture());
});
match join_handle.join() {
    Err(e) => {
        let backtrace = e
            .downcast::<Backtrace>()
            .expect("downcast");
        println!("{}", backtrace);
    }
    Ok(_) =>{
    }
}
1 Like

As mentioned above, this is for GUI programs. There is no console output because there is no console window.

Redirect stdout and stderr to a file, like everyone else.

Windows is the only sore spot, which needs to use either the EventViewer or Event Tracing for Windows when #[cfg(windows_subsystem = "windows")] is used (literally no console at all, and no standard stream handles to redirect).

  • For EventViewer: A combination of log-panics and eventlog. (Requires admin privileges to register the event source, e.g. during application installation.)
  • For ETW: A combination of tracing-panic and tracing-etw. (Requires starting a tracing session with external tools.)

For GUI applications I would recommend setting a panic hook using std::panic::set_hook. In this panic hook you have access to both the panic message and can capture a backtrace. You could then either log it somewhere or show a popup to the user. Once you return from the panic hook, the panic will unwind like it would otherwise. Unless you call the default panic hook, you will not get the regular panic message with optional backtrace though, but for a GUI application you wouldn't get that anyway.

Edit: This is basically what parasyte suggested. The log-panics and tracing-panic crates both register a panic hook.

1 Like

And now that I think about it, log-panics with a simple file logger can be used on all platforms without the explicit need for users to pipe or redirect streams for the command, and without the need to open the Windows Event Viewer specifically. It isn't as friendly as showing a window containing a backtrace, but it would be persistent and also gracefully handle degenerate cases like panic in the main thread and panic-while-panicking [1].


  1. Well, maybe only some degenerate cases. If the log provider recursively logs, you will still have severe problems. ↩︎

There are people called "users" who only know about stuff that appears in windows.

Question: to just pop up an alert box after the full GUI has failed, I currently use the "rfd" crate, which, in addition to file dialogs, has a dialog box option. But apparently the "rfd" dialog box, unlike the file dialog, doesn't work on Wayland. Probably no one converted it. Any simple alternatives for a single final dialog box?

On Linux rfd uses GTK3 for showing message dialogs. However this only works if you enable the gtk3 feature of rfd to actually link against GTK3. By default rfd uses the xdg desktop portals dbus interface which only supports showing file dialogs, not message dialogs.

Actually, looking at the source code rather than the docs it seems like if you don't have the GTK3 backend enabled, rfd will try to use zenity. This is an external program for showing dialogs which most distros don't install by default AFAIK.

I guess it would be nice if rfd could also support GTK4 and maybe even support dlopening whichever GTK version is installed on the local system rather than having a hard dependency on a specific GTK version. That way the GTK backend could always be enabled as fallback for when the xdg desktop portals backend is unable to do something.

It may be worth considering a crash reporter. For instance, what Firefox does: Porting a cross-platform GUI application to Rust - Mozilla Hacks - the Web developer blog

For dialog box alternatives, I know of xdialog, which uses fltk under the hood. (It's an extra C and C++ dependency. Requires CMake et al.)