use futures::prelude::*;
use std::panic::{AssertUnwindSafe, UnwindSafe};
pub async fn try_finally<B, F, BFut, FFut, Out, Err>(body: B, finally: F) -> Result<Out, Err>
where
B: FnOnce() -> BFut,
B: UnwindSafe,
BFut: Future<Output = Result<Out, Err>>,
F: FnOnce() -> FFut,
FFut: Future<Output = Result<(), Err>>,
{
let body = AssertUnwindSafe(body());
let body_res = body.catch_unwind().await;
let finally_res = finally().await;
match (body_res, finally_res) {
(Err(cause), _) => std::panic::resume_unwind(cause),
(_, Err(finally_err)) => Err(finally_err),
(Ok(res), Ok(())) => res,
}
}
#[cfg(test)]
mod test {
use super::*;
use anyhow::{bail, Ok};
#[tokio::test]
async fn test_finally_runs_after_success() {
let mut cleaned_up = false;
let res = try_finally(
|| async { Ok("ok!") },
|| async {
cleaned_up = true;
Ok(())
},
)
.await
.unwrap();
assert_eq!(res, "ok!");
assert!(cleaned_up);
}
#[tokio::test]
async fn test_finally_runs_after_failure() {
let mut cleaned_up = false;
let err = try_finally(
|| async {
bail!("oh no!");
#[allow(unreachable_code)]
Ok(())
},
|| async {
cleaned_up = true;
Ok(())
},
)
.await
.unwrap_err();
assert_eq!(err.to_string(), "oh no!");
assert!(cleaned_up);
}
#[tokio::test]
#[should_panic(expected = "in the cleanup!")]
async fn test_finally_runs_after_panic() {
try_finally(
|| async {
panic!("at the disco!");
#[allow(unreachable_code)]
Ok(())
},
|| async {
panic!("in the cleanup!")
},
)
.await
.unwrap();
}
#[tokio::test]
async fn test_finally_errors_are_propagated() {
let err = try_finally(
|| async { Ok("sucess!") },
|| async { bail!("cleanup failed!") },
)
.await
.unwrap_err();
assert_eq!(err.to_string(), "cleanup failed!");
}
#[tokio::test]
async fn test_finally_errors_mask_body_errors() {
let err = try_finally(
|| async {
bail!("body failed!");
#[allow(unreachable_code)]
Ok(())
},
|| async { bail!("cleanup failed!") },
)
.await
.unwrap_err();
assert_eq!(err.to_string(), "cleanup failed!");
}
}