# async-safe-defer
[](https://github.com/rust-dd/async-safe-defer/actions/workflows/ci.yml)
[](https://crates.io/crates/async-safe-defer)
[](https://docs.rs/async-safe-defer)
Runtime-independent scope guards for synchronous and explicitly awaited
asynchronous cleanup.
- zero normal dependencies; no executor is selected or spawned
- `#![no_std]` synchronous guards and fixed-capacity async scopes
- local and `Send` heap-backed async scopes behind `alloc`
- always, success, unwind, and custom synchronous drop strategies
- no `unsafe` Rust, enforced with `#![forbid(unsafe_code)]`
The word "safe" means that this crate contains no `unsafe` Rust. It does not
mean cancellation-safe async drop: asynchronous cleanup must still be driven to
completion by the caller.
## Synchronous cleanup
The `defer!` macro binds a guard to the surrounding lexical scope. Its action
runs on normal return and during panic unwinding whenever destructors run. The
synchronous defer macros require their cleanup body to evaluate to `()`; they
do not silently discard non-unit or fallible results. Handle such results inside
the body explicitly.
```rust
use async_safe_defer::defer;
use std::cell::Cell;
let cleaned = Cell::new(false);
{
defer!(cleaned.set(true));
assert!(!cleaned.get());
}
assert!(cleaned.get());
```
[`DeferGuard`](https://docs.rs/async-safe-defer/latest/async_safe_defer/struct.DeferGuard.html)
can be disarmed or run early. Once initialized inside a future, a guard also
runs when that future is cancelled and dropped. If cancellation happens before
execution reaches the guard binding, no guard exists yet.
A synchronous cleanup panic propagates normally. If cleanup panics while
another panic is already unwinding, Rust's double-panic behavior can abort the
process.
### Guarded values
[`ScopeGuard`](https://docs.rs/async-safe-defer/latest/async_safe_defer/struct.ScopeGuard.html)
owns a protected value and exposes it through `Deref` and `DerefMut`. Its action
receives the final value on drop.
```rust
use async_safe_defer::guard;
use std::cell::RefCell;
let finalized = RefCell::new(Vec::new());
{
let mut values = guard(vec![1], |values| {
finalized.replace(values);
});
values.push(2);
assert_eq!(&*values, &[1, 2]);
}
assert_eq!(finalized.into_inner(), vec![1, 2]);
```
Use `into_inner` to recover the value without running the action, `into_parts`
to recover both pieces, or `run_now` to execute immediately.
The crate provides no manual or `unsafe` `Send`/`Sync` implementations for its
guards. Their auto traits follow the stored action and, for `ScopeGuard`, the
guarded value; a guard is `Sync` only when those fields are `Sync`.
### Conditional drop strategies
The optional `std` feature provides success-only and unwind-only guards and
macros.
```rust
# #[cfg(feature = "std")]
# {
use async_safe_defer::{defer_on_success, defer_on_unwind};
use std::cell::Cell;
let committed = Cell::new(false);
let rolled_back = Cell::new(false);
{
defer_on_unwind!(rolled_back.set(true));
defer_on_success!(committed.set(true));
}
assert!(committed.get());
assert!(!rolled_back.get());
# }
```
For these policies, success means that the thread is not unwinding. Returning
`Result::Err` still counts as success. Implement [`Strategy`](https://docs.rs/async-safe-defer/latest/async_safe_defer/trait.Strategy.html)
for a custom drop policy.
## Asynchronous cleanup
`async_scope!` accepts ordinary `|| async { ... }` closures, preserves the body
value, and runs cleanup before propagating `?` or an inner `return`. Closures
and futures may borrow values outside the scope invocation.
```rust
# #[cfg(feature = "alloc")]
# mod local_scope_example {
use async_safe_defer::async_scope;
# async fn close_remote_resource() {}
# async fn do_work() -> Result<u32, &'static str> { Ok(42) }
# async fn example() -> Result<u32, &'static str> {
let value = async_scope!(scope, {
scope.defer(|| async {
close_remote_resource().await;
});
do_work().await
})
.await?;
assert_eq!(value, 42);
# Ok(value)
# }
# }
```
A cleanup stack can act as rollback by clearing it only after commit:
```rust
# #[cfg(feature = "alloc")]
# mod rollback_example {
use async_safe_defer::async_scope;
use core::cell::Cell;
# async fn update() -> Result<(), &'static str> { Err("update failed") }
# async fn rollback(flag: &Cell<bool>) { flag.set(true); }
# async fn transaction(flag: &Cell<bool>) -> Result<(), &'static str> {
async_scope!(scope, {
scope.defer(|| rollback(flag));
update().await?;
scope.clear();
Ok(())
})
.await
# }
# }
```
The local scope accepts `Rc`, `RefCell`, and non-`Send` futures. Use
`send_async_scope!` when registered cleanup closures and futures must be
`Send`. Whether the complete returned future is `Send` also depends on its
body, output, and captures.
```rust
# #[cfg(feature = "alloc")]
# mod send_scope_example {
use async_safe_defer::send_async_scope;
# async fn flush() {}
# async fn example() {
send_async_scope!(scope, {
scope.defer(|| async { flush().await });
})
.await;
# }
# }
```
For manual lifecycle control, `run(&mut self)` borrows the scope and is resumable
if its returned future is dropped while pending. `finish(self)` consumes the
scope and releases captured borrows. A run poll continues through LIFO actions
until one returns `Pending`, so one poll may complete every immediately-ready
action.
## Allocator-free asynchronous cleanup
`FixedAsyncScope` and `FixedSendAsyncScope` store caller-owned pinned future
references in an inline array. The registry itself performs no heap allocation
and accepts heterogeneous futures.
```rust
use async_safe_defer::fixed::FixedAsyncScope;
use core::pin::pin;
# async fn close_socket() {}
# async fn flush_log() {}
# async fn example() {
let mut close = pin!(close_socket());
let mut flush = pin!(flush_log());
let mut scope = FixedAsyncScope::<2>::new();
scope.try_defer(close.as_mut()).expect("slot available");
scope.try_defer(flush.as_mut()).expect("slot available");
scope.finish().await;
# }
```
The pinned futures must outlive the scope, so create them first. `try_defer`
returns the rejected reference in `CapacityError` instead of panicking. A
registered future may allocate internally; only the registry is guaranteed
allocator-free.
Use `FixedAsyncScope` for local cleanup futures, including non-`Send` borrows.
Use `FixedSendAsyncScope` when every registered future must be `Send` and the
fixed scope's run or finish future may need to cross thread boundaries.
## Compared with `scopeguard`
[`scopeguard`](https://docs.rs/scopeguard/latest/scopeguard/) remains a focused,
low-MSRV synchronous guard crate. `async-safe-defer` provides the same central
synchronous patterns and extends them with explicit async cleanup.
| Always-run synchronous defer | yes | yes |
| Guarded value with mutable access and extraction | yes | yes |
| Success and unwind strategies | `std` feature | default `use_std` feature |
| Custom drop strategy | yes | yes |
| Awaited sequential LIFO cleanup | yes | no |
| Borrowed non-`'static` async cleanup | yes | no |
| Allocator-free fixed async registry | yes | no |
| Normal dependencies | none | none |
| MSRV | Rust 1.83 | Rust 1.20 |
Choose `scopeguard` when only synchronous cleanup and a much older compiler are
required. Choose this crate when one dependency should cover synchronous guards
and runtime-neutral async finalization.
## Execution guarantees
The normal-completion guarantees below assume the returned future is polled to
completion. Run futures poll LIFO actions until one returns `Pending`, so a
single poll can execute every immediately-ready action.
| Body completes normally | all actions run |
| Body returns `Result::Err` through `?` | all actions run, then the error is returned |
| `return` inside the macro body | all actions run, then the value is returned |
| A borrowed heap or fixed `run()` future is dropped while pending | the current action remains registered; a later run resumes it after newer LIFO entries |
| A heap or fixed `finish()` future, or an async-scope macro future is dropped | remaining registrations are discarded; caller-owned fixed futures remain owned by the caller |
| Body panic | remaining async actions are discarded |
| Cleanup panic | the current registration is lost; borrowed heap and fixed `run` retain older entries, while consuming `finish` calls and macros discard them |
Rust has no stable, runtime-independent way to await work from a normal `Drop`
implementation. Do not use async cleanup to restore memory-safety invariants or
release mandatory resources under arbitrary task cancellation. Use a
synchronous guard for the cancellation fallback when one is required.
## Features
| `alloc` | yes | `LocalAsyncScope`, `SendAsyncScope`, async scope macros |
| `std` | no | success and unwind strategies, constructors, and macros |
| no features | no | sync always-strategy guards and `fixed::{FixedAsyncScope, FixedSendAsyncScope}` |
```text
cargo check --no-default-features
cargo check --no-default-features --features std
```
## Cost model and limits
- `LocalAsyncScope` and `SendAsyncScope` use one boxed wrapper per action plus
amortized `Vec` storage.
- `FixedAsyncScope` and `FixedSendAsyncScope` perform no heap allocation for
registry storage but require explicit pinning and lifetimes.
- Async actions return `()`. Logging, retry, and error handling happen inside
each action; errors are not aggregated.
- Cleanup is sequential, not parallel, and no timeout policy is imposed.
## Migrating from 0.1
- **Breaking:** the misleading `no_alloc` feature, `no_alloc` module,
`AsyncScopeNoAlloc`, `DeferredFn`, and `no_alloc_async_scope!` were removed.
Use `fixed::FixedAsyncScope` or `fixed::FixedSendAsyncScope` for a fixed
registry that performs no heap allocation.
- `AsyncScope::defer` accepts ordinary `FnOnce() -> Future` factories without
caller-side boxing.
- **Breaking:** heap-backed `AsyncScope::run` now borrows `&mut self` and is
resumable. Use `finish(self)` when the cleanup future must consume and own the
scope; there is no `run_mut` alias.
- A dependency using `default-features = false` must enable `alloc` explicitly
to access heap-backed scopes.
- **Breaking:** async scope macros now return the body value and do not let an
inner `return` or `?` bypass cleanup; internally they consume their scope with
`finish`.
- **Breaking:** synchronous defer macros no longer discard non-unit or fallible
cleanup results. Handle or explicitly discard those results in the cleanup
body so it evaluates to `()`.
- The correct Rust import has always been `async_safe_defer`; older README
examples used the wrong name.
The minimum supported Rust version is 1.83. The crate is licensed under MIT.