Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

2.3 · Panic Infrastructure: Hooks, Unwind, and Abort

Domain 2 — Error Handling Ecosystem Duration: ~15 minutes Library components: std::panic, std::panic::catch_unwind, std::panic::resume_unwind, std::panic::set_hook, std::panic::take_hook, std::panic::PanicHookInfo, std::panic::UnwindSafe, std::panic::RefUnwindSafe, std::panic::AssertUnwindSafe

Introduction

A panic is the Rust mechanism for an unrecoverable error. After such an error, the program cannot reasonably continue. Examples are an index that is out of bounds, an integer overflow in debug mode, and an explicit panic!. For specific use cases, the standard library gives you several tools that control the panic mechanism.

This tutorial shows:

  • How catch_unwind intercepts a panic at a boundary.
  • How set_hook and take_hook give you custom panic output.
  • The PanicHookInfo type and its contents.
  • UnwindSafe, RefUnwindSafe, and AssertUnwindSafe.
  • How resume_unwind starts a panic again with the original payload.
  • When each of these tools is applicable.

panic! and catch_unwind

panic! first calls the panic hook, and the default hook prints the panic message to stderr. Then panic! unwinds the call stack and runs the Drop implementations of the values on that stack. The unwind stops at one of two points:

  1. A catch_unwind call intercepts the panic and returns Err(Box<dyn Any + Send>).
  2. No catch_unwind call is on the stack, and the panic ends the thread.

Figure: Panic Unwinding Flow

catch_unwind is not for general error handling. Use Result for that. catch_unwind has these uses:

  • Thread pools: a worker that panics must not stop the whole process.
  • FFI boundaries: C code cannot handle Rust panics. With catch_unwind, you convert a panic to an error code.
  • Test infrastructure: the test runner continues with the other tests when some tests panic.
use std::panic;

// Panics when `b` is 0. The panic message has a format argument.
fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("division by zero: a={a}");
    }
    a / b
}

// catch_unwind returns Ok(value) if the closure returns, and Err(payload) if it panics.
let ok = panic::catch_unwind(|| divide(10, 2));
assert_eq!(ok.unwrap(), 5);

let caught = panic::catch_unwind(|| divide(10, 0));
assert!(caught.is_err()); // the panic stops here, and the program continues

// The payload is a Box<dyn Any + Send>. Downcast it to get the message.
let payload = panic::catch_unwind(|| divide(5, 0)).unwrap_err();
if let Some(msg) = payload.downcast_ref::<String>() {
    // A message with format arguments is a String: this branch runs.
    println!("panic payload (String): {msg:?}");
} else if let Some(msg) = payload.downcast_ref::<&str>() {
    // A literal message, such as panic!("text"), is a &'static str.
    println!("panic payload (&str):   {msg:?}");
}

The supervisor pattern processes a batch in which individual items may panic:

// process_item(item) panics for 0 and for a negative item.
// For each other item, it returns item * 2.
let items = [3, 0, 5, -1, 4];
for &item in &items {
    match panic::catch_unwind(|| process_item(item)) {
        Ok(v) => println!("item {item} → {v}"),
        Err(payload) => {
            // Get the message from a String payload or from a &str payload.
            let msg = payload
                .downcast_ref::<String>()
                .map(String::as_str)
                .or_else(|| payload.downcast_ref::<&str>().copied())
                .unwrap_or("(unknown panic)");
            println!("item {item} panicked: {msg}");
        }
    }
}
// Items 3, 5, and 4 succeed. Items 0 and -1 panic, but the loop continues.

todo!(), unreachable!(), and out-of-bounds indexing all cause panics that catch_unwind can catch.

02_11_panic_basics.rs prints:

catch_unwind (no panic): Ok(5)
catch_unwind (panic):    Err(Any { .. })
panic payload (String): "division by zero: a=5"

item 3 → 6
item 0 panicked: got zero, cannot continue
item 5 → 10
item -1 panicked: negative input: -1
item 4 → 8

todo! panic caught: true
unreachable! panic caught: true

All assertions passed.

This block shows stdout only. The default panic hook also prints the message of each caught panic to stderr.

Panic Hooks: set_hook and take_hook

By default, a panic prints a message to stderr, and optionally a backtrace. set_hook replaces this behavior fully.

use std::panic::{self, PanicHookInfo};

// take_hook removes the current hook (here, the default hook) and returns it.
// Keep the returned hook, so that you can restore it later.
let default_hook = panic::take_hook();

// Install your own hook. The runtime calls it one time for each panic.
panic::set_hook(Box::new(|info: &PanicHookInfo<'_>| {
    // info.payload()  is the panic payload (usually &str or String)
    // info.location() is the file, line, and column of the panic
    // This chain gets the message from a String payload or from a &str payload.
    // Since 1.91, info.payload_as_str() does the same in one call.
    let msg = info.payload()
        .downcast_ref::<String>().map(String::as_str)
        .or_else(|| info.payload().downcast_ref::<&str>().copied())
        .unwrap_or("(non-string payload)");

    match info.location() {
        Some(loc) => eprintln!("PANIC at {}:{}: {}", loc.file(), loc.line(), msg),
        None      => eprintln!("PANIC (no location): {msg}"),
    }
}));

// This panic calls your hook, not the default hook. catch_unwind stops the unwind.
let _ = panic::catch_unwind(|| panic!("something went wrong"));

// Restore the default hook.
panic::set_hook(default_hook);

take_hook() is the only function that gives you the current hook. It removes the hook from the runtime and returns it. To add to the default behavior (and not replace it), do these steps:

  1. Take the default hook.
  2. Call the default hook in your custom hook.
  3. Install your custom hook.

PanicHookInfo (replaces PanicInfo since 1.81)

std::panic::PanicHookInfo has these methods:

  • payload() returns &(dyn Any + Send): the value that panic! received.
  • location() returns Option<&'static Location<'static>>: the file name, the line, and the column.
  • can_unwind() returns bool: true if the panic unwinds, false if it aborts. This method is still unstable in 1.99 (feature panic_can_unwind).

Rust 1.81 added the name PanicHookInfo, and Rust 1.82 deprecated the old name std::panic::PanicInfo. The new name prevents confusion with core::panic::PanicInfo. A #[panic_handler] function in a no_std environment receives that core type.

The hook in 02_12_panic_hooks.rs does not print. It adds one line for each panic to a shared Vec<String>. The example prints:

Captured 3 panic events:
  [0] panic at domain-02-error-handling/examples/src/bin/02_12_panic_hooks.rs:48: first failure
  [1] panic at domain-02-error-handling/examples/src/bin/02_12_panic_hooks.rs:49: second failure
  [2] panic at domain-02-error-handling/examples/src/bin/02_12_panic_hooks.rs:52: index out of bounds: the len is 0 but the index is 10

Hook restored. Log count unchanged: 3

All assertions passed.

loc.file() returns the path that the compiler received. Cargo compiles the example from the workspace root, so the path is relative to that directory.

UnwindSafe, RefUnwindSafe, and AssertUnwindSafe

catch_unwind requires an UnwindSafe closure. This trait marks the types that cannot leave shared state invalid if a panic occurs in the middle of the closure.

Which types are UnwindSafe?

  • i32, bool, String, and each type that contains only UnwindSafe types: yes.
  • &mut T: no. A panic during a change through a &mut T can leave the referenced value partially changed.
  • &RefCell<T>: no. A closure can also change the value through this shared reference, and RefCell does not record that a panic stopped a change.
  • &Mutex<T>: yes. A panic while the closure holds the guard poisons the Mutex, and subsequent lock calls return a PoisonError.

Figure: Is My Type UnwindSafe?

AssertUnwindSafe: the explicit override

When you know that a captured value is safe, although its type does not implement UnwindSafe, wrap the closure:

use std::panic::{self, AssertUnwindSafe};

let mut counter = 0_u32;

// The closure captures `&mut counter`, so the closure is not UnwindSafe.
// The wrapper around the whole closure tells the compiler that you checked the safety.
let ok = panic::catch_unwind(AssertUnwindSafe(|| {
    counter += 10; // the only change: a panic cannot leave it partially done
    counter
}));
assert_eq!(ok.unwrap(), 10); // the closure returned the new value of `counter`

AssertUnwindSafe is your statement to the compiler that you verified the closure. The statement says that a panic in this closure leaves no broken invariant in the captured state. The compiler does not check the statement. If the statement is wrong, the result can be a logic error, but not undefined behavior in safe code.

Since Rust 1.96, AssertUnwindSafe<T> also implements From<T> for each T that is UnwindSafe. Thus let wrapped: AssertUnwindSafe<u32> = value.into(); makes the wrapper through the standard conversion traits. This is convenient in generic code that already accepts impl Into<AssertUnwindSafe<T>>.

A note on RefCell: a temporary borrow() or borrow_mut() guard drops at the semicolon of its statement. If no guard is alive when the panic occurs, no change of the value is in progress. Then the RefCell is safe to use again after catch_unwind. Use AssertUnwindSafe to tell the compiler about that reasoning.

02_13_unwind_safe.rs prints:

catch_unwind result: true
cell after catch:    1

catch_unwind with &mut (ok): Ok(10)
catch_unwind with &mut (panic): true

AssertUnwindSafe return: Ok("hello")
AssertUnwindSafe via From: 15

All assertions passed.

resume_unwind: Re-panicking with the Original Payload

catch_unwind catches a panic and gives you the payload. resume_unwind starts the panic again with that same payload, as if the original panic did not stop:

use std::{panic, thread};

fn spawn_worker(value: i32) -> thread::JoinHandle<i32> {
    thread::spawn(move || {
        // result: Result<i32, Box<dyn Any + Send>>
        let result = panic::catch_unwind(move || {
            // The worker logic. It panics when `value` is 0.
            if value == 0 {
                panic!("worker received zero");
            }
            value * 3
        });
        // This is the place to log or to clean up before the panic continues.
        match result {
            Ok(v)        => v,
            // Start the panic again in this worker thread, with the original payload.
            // join() in the parent thread then returns Err(payload).
            Err(payload) => panic::resume_unwind(payload),
        }
    })
}

resume_unwind(payload) and panic!(...) are different:

  • resume_unwind keeps the type and the message of the original payload.
  • panic! starts a new panic with a new message.

When to use resume_unwind vs. convert to error

SituationRecommendation
A thread pool must propagate the panic to the callerCall resume_unwind after the logging and the cleanup
FFI boundary (extern "C")Never resume the panic. Convert it to an error code
Test runnerCall resume_unwind to keep the original failure message
Custom retry logicExamine the payload. Then resume the panic or discard the payload

02_14_resume_unwind.rs prints:

=== Logging runner ===
[add] completed normally
[divide] panicked: division by zero

=== Cross-thread resume_unwind ===
worker(7) → 21
worker(0) panicked: worker received zero

=== FFI boundary pattern ===
ffi_result code: -1

All assertions passed.

Summary

ToolPurpose
panic!Signals an unrecoverable error
catch_unwindIntercepts a panic at a boundary (thread pool, FFI)
resume_unwindStarts the panic again with the original payload, after you examine it
set_hookReplaces the default panic output
take_hookRemoves the current hook and returns it, so that you can restore it
PanicHookInfoThe payload and the location that the hook function receives
UnwindSafeMarker: the type cannot leave invalid state after a panic
RefUnwindSafeMarker: the same property for &T
AssertUnwindSafeWrapper: you state that you verified the safety

The rule: use Result for expected failures. Use catch_unwind only at controlled boundaries that must stop a panic from untrusted or isolated code.

Code Examples

FileDescription
02_11_panic_basics.rspanic!, catch_unwind, payload extraction, supervisor pattern
02_12_panic_hooks.rsset_hook, take_hook, PanicHookInfo location and payload
02_13_unwind_safe.rsUnwindSafe, RefUnwindSafe, AssertUnwindSafe, RefCell interaction, From<T> constructor (1.96)
02_14_resume_unwind.rsresume_unwind, thread-pool pattern, FFI boundary pattern