Catching a panic and saving a backtrace

I'm trying to catch panics in a multi-thread GUI program. I want to get a backtrace of the panicking thread, log it (there's no text console) and then tell all the other threads to shut down cleanly.
So I tried catch_unwind and backtrace.

Catching the panic works, but it seems to be caught too late. When I get the backtrace, it's a backtrace of the internals of the panic system, not the program with the problem. This is useless.

What's the right way to do this?

Backtrace: Backtrace [
{ fn: "playground::main", file: "./src/main.rs", line: 19 },
{ fn: "core::ops::function::FnOnce::call_once", file: "/rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/core/src/ops/function.rs", line: 250 },
{ fn: "std::sys_common::backtrace::__rust_begin_short_backtrace", file: "/rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/sys_common/backtrace.rs", line: 121 },
{ fn: "std::rt::lang_start::{{closure}}", file: "/rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/rt.rs", line: 166 },
{ fn: "core::ops::function::impls::<impl core::ops::function::FnOnce<A> for &F>::call_once", file: "/rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/core/src/ops/function.rs", line: 287 },
{ fn: "std::panicking::try::do_call", file: "/rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panicking.rs", line: 483 },
{ fn: "std::panicking::try", file: "/rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panicking.rs", line: 447 },
{ fn: "std::panic::catch_unwind", file: "/rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panic.rs", line: 140 },
{ fn: "std::rt::lang_start_internal::{{closure}}", file: "/rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/rt.rs", line: 148 },
{ fn: "std::panicking::try::do_call", file: "/rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panicking.rs", line: 483 },
{ fn: "std::panicking::try", file: "/rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panicking.rs", line: 447 },
{ fn: "std::panic::catch_unwind", file: "/rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panic.rs", line: 140 },
{ fn: "std::rt::lang_start_internal", file: "/rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/rt.rs", line: 148 },
{ fn: "std::rt::lang_start", file: "/rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/rt.rs", line: 165 },
{ fn: "main" }, 
{ fn: "__libc_start_main" }, { fn: "_start" }]

The backtrack will be captured when you call Backtrace::force_capture(), which is part of main() further down. That's why you see main() at the top of the stack. The entries above it are actually for the catch_panic() that main() runs inside.

If you want to get a backtrace from the panic, you'll need to use std::panic::set_hook() to make sure your backtrace capturing code is fired when the panic is triggered. You'll also need to come up with an alternative way to "smuggle" the backtrace back to the caller, because panic hooks can't modify the panic payload or otherwise modify what is returned by catch_panic().

1 Like

Here's a simple panic hook example

Playground

use std::{any::Any, backtrace::Backtrace, panic};

/// Worker, does something useful and panics
fn called_by_worker() {
    panic!("oh no!");
}

fn worker() {
    called_by_worker();
}

fn handle_panic(payload: &(dyn Any + Send), backtrace: Backtrace) {
    eprint!("Panicked: ");
    if let Some(string) = payload.downcast_ref::<String>() {
        eprintln!("{string}");
    } else if let Some(str) = payload.downcast_ref::<&'static str>() {
        eprintln!("{str}")
    } else {
        eprintln!("{payload:?}")
    }

    eprintln!("Backtrace: {backtrace:#?}");
}

fn main() {
    panic::set_hook(Box::new(|info| {
        let backtrace = std::backtrace::Backtrace::force_capture();
        handle_panic(info.payload(), backtrace)
    }));
    let result = panic::catch_unwind(|| {
        worker();
    });

    println!("{result:?}")
}

When you set the panic hook you looks the default one though. That's probably not a problem if you're working with a GUI app since the default one basically just prints a backtrace to the console.

Ok, a panic hook version:

This captures something verbose, but useful:

Backtrace:    0: playground::main::{{closure}}
             at ./src/main.rs:16:25
   1: <alloc::boxed::Box<F,A> as core::ops::function::Fn<Args>>::call
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/alloc/src/boxed.rs:2002:9
   2: std::panicking::rust_panic_with_hook
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panicking.rs:692:13
   3: std::panicking::begin_panic_handler::{{closure}}
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panicking.rs:577:13
   4: std::sys_common::backtrace::__rust_end_short_backtrace
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/sys_common/backtrace.rs:137:18
   5: rust_begin_unwind
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panicking.rs:575:5
   6: core::panicking::panic_fmt
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/core/src/panicking.rs:64:14
   7: playground::called_by_worker
             at ./src/main.rs:5:5
   8: playground::worker
             at ./src/main.rs:9:5
   9: playground::main::{{closure}}
             at ./src/main.rs:21:9
  10: std::panicking::try::do_call
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panicking.rs:483:40
  11: __rust_try
  12: std::panicking::try
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panicking.rs:447:19
  13: std::panic::catch_unwind
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panic.rs:140:14
  14: playground::main
             at ./src/main.rs:20:18
  15: core::ops::function::FnOnce::call_once
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/core/src/ops/function.rs:250:5
  16: std::sys_common::backtrace::__rust_begin_short_backtrace
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/sys_common/backtrace.rs:121:18
  17: std::rt::lang_start::{{closure}}
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/rt.rs:166:18
  18: core::ops::function::impls::<impl core::ops::function::FnOnce<A> for &F>::call_once
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/core/src/ops/function.rs:287:13
  19: std::panicking::try::do_call
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panicking.rs:483:40
  20: std::panicking::try
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panicking.rs:447:19
  21: std::panic::catch_unwind
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panic.rs:140:14
  22: std::rt::lang_start_internal::{{closure}}
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/rt.rs:148:48
  23: std::panicking::try::do_call
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panicking.rs:483:40
  24: std::panicking::try
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panicking.rs:447:19
  25: std::panic::catch_unwind
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panic.rs:140:14
  26: std::rt::lang_start_internal
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/rt.rs:148:20
  27: std::rt::lang_start
             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/rt.rs:165:17
  28: main
  29: __libc_start_main
  30: _start

Panic occurred: Any { .. }

This area of Rust error handling looks unfinished.

  • Backtrace is supposed to be iterable, but that's only available in nightly. The only thing you can do with a Backtrace right now is format it. You can't even clone it. But at least "Display" gives you something with line breaks.
  • catch_unwind returns an Err(Any), which just displays as "Any..." Not too useful.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.