How do I use panic::catch_unwind with asynchronous code?
Asked Answered
S

3

7

When working with synchronous code, I can use panic::catch_unwind like this:

#[actix_rt::test]
async fn test_sync() -> Result<(), Error> {
    println!("before catch_unwind");
    let sync_result = panic::catch_unwind(|| {
        println!("inside sync catch_unwind");
        panic!("this is error")
    });
    println!("after catch_unwind");

    assert!(sync_result.is_ok());

    Ok(())
}

How do I do the same when working with async code that is executed inside the catch_unwind block? I can't figure out how to run the block while also being able to run some code after the block and finally assert the result.

This is what I have so far:

#[actix_rt::test]
async fn test_async() -> Result<(), Error> {
    println!("before catch_unwind");
    let async_result = panic::catch_unwind(|| async {
        println!("inside async catch_unwind");
        panic!("this is error")
    }).await;
    println!("after catch_unwind");

    assert!(async_result.is_ok());

    Ok(())
}
Snakeroot answered 29/7, 2020 at 17:40 Comment(0)
H
13

I would not attempt to use them directly. Instead, use FutureExt::catch_unwind and StreamExt::catch_unwind.

use futures::FutureExt; // 0.3.5

#[tokio::test]
async fn test_async() -> Result<(), Box<dyn std::error::Error>> {
    println!("before catch_unwind");

    let may_panic = async {
        println!("inside async catch_unwind");
        panic!("this is error")
    };

    let async_result = may_panic.catch_unwind().await;

    println!("after catch_unwind");

    assert!(async_result.is_ok());

    Ok(())
}
Hadlock answered 29/7, 2020 at 17:47 Comment(3)
Thank you, this works great! However, for more complex examples I do get errors like "may not be safely transferred across an unwind boundary" when calling async functions in libraries such as sqlx :/Snakeroot
This works when may_panic, as shown in the example is a future created in this module. However it does not work if the code in may_panic waits on another future which was returned by another method where futures:FutureExt was not brought into scope. In that case, catch_unwind complains that the future is not transferrable across a unwind boundary because the future returned by the async function itself waits on that other future which is not transferrable. Any ideas what can be done to resolve this?Skutchan
I've had some luck wrapping the function: let may_panic = async || func(); In my case it was for a trait and I had to add constraint where Self: RefUnwindSafeDiclinous
S
4

I ran into this problem and Shepmaster's answer did work partially. I got a lot of errors with very complex descriptions about unwinding and unsafe transferring variables. In your comment to the accepted answer, you also addressed this problem.

This workaround below solved the problem for me. I do not recommend it to use it outside of tests, since this way could be expensive. It uses a Mutex and the current Runtime (Handle).

fn main() {}

#[cfg(test)]
mod test {
    #[tokio::test]
    async fn test() {
        env_logger::init();
        // This variable represents your complex type
        // Note that this can be a combination of types if you use a tuple or something else
        let my_complex_type = 1;

        // Wrap it all in a std::sync::Mutex
        let mutex = std::sync::Mutex::new(my_complex_type);

        // Pass the mutex in the panic unwind
        assert!(std::panic::catch_unwind(|| {
            // Now you can work with your complex type
            let my_complex_type = mutex.lock().unwrap();

            // Enter the runtime
            let handle = tokio::runtime::Handle::current();

            handle.enter();

            futures::executor::block_on(do_something(*my_complex_type));
        }).is_err());
    }

    async fn do_something(t: i32) {
        panic!();
    }
}
Sheng answered 8/3, 2021 at 11:30 Comment(1)
I also had a pretty wild compiler error trying to use catch_unwind. I ended up using #[should_panic(expected = "...")] which did the trick for me.Elevation
S
2

To avoid the "fun" of a hundred or so "may not be safely transferred across an unwind boundary" errors, you can use the AssertUnwindSafe wrapper type to tell the compiler to just STFU. There isn't really a good async-friendly example in the official docs, but here's how I got an async test that needed post-run cleanup to work:

use futures_util::FutureExt;                                                                                                                                                                                                                   
use std::panic::AssertUnwindSafe;

#[actix_rt::test]                                                                                                                                                                                                                              
async fn password_auth() {                                                                                                                                                                                                                     
    let b = Browser::new().await;                                                                                                                                                                                                              
                                                                                                                                                                                                                                               
    let result = AssertUnwindSafe(run_password_auth(b.clone())).catch_unwind().await;                                                                                                                                                          
                                                                                                                                                                                                                                               
    b.close().await;                                                                                                                                                                                                                           
                                                                                                                                                                                                                                               
    if let Err(e) = result {                                                                                                                                                                                                                   
        panic!("{:?}", e);                                                                                                                                                                                                                     
    }                                                                                                                                                                                                                                          
}

async fn run_password_auth(b: Browser) {
    // .await away!
}                                                                                                                                                                                                                                              
Sauncho answered 21/5, 2024 at 1:11 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.