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
Sendheap-backed async scopes behindalloc - always, success, unwind, and custom synchronous drop strategies
- no
unsafeRust, 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.
use defer;
use Cell;
let cleaned = new;
assert!;
DeferGuard
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
owns a protected value and exposes it through Deref and DerefMut. Its action
receives the final value on drop.
use guard;
use RefCell;
let finalized = new;
assert_eq!;
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.
#
#
For these policies, success means that the thread is not unwinding. Returning
Result::Err still counts as success. Implement Strategy
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.
#
#
A cleanup stack can act as rollback by clearing it only after commit:
#
#
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.
#
#
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.
use FixedAsyncScope;
use pin;
# async
# async
# async
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 remains a focused,
low-MSRV synchronous guard crate. async-safe-defer provides the same central
synchronous patterns and extends them with explicit async cleanup.
| Capability | async-safe-defer |
scopeguard 1.2 |
|---|---|---|
| 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.
| Event | Async cleanup behavior |
|---|---|
| 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
| Feature | Default | API |
|---|---|---|
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} |
cargo check --no-default-features
cargo check --no-default-features --features std
Cost model and limits
LocalAsyncScopeandSendAsyncScopeuse one boxed wrapper per action plus amortizedVecstorage.FixedAsyncScopeandFixedSendAsyncScopeperform 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_allocfeature,no_allocmodule,AsyncScopeNoAlloc,DeferredFn, andno_alloc_async_scope!were removed. Usefixed::FixedAsyncScopeorfixed::FixedSendAsyncScopefor a fixed registry that performs no heap allocation. AsyncScope::deferaccepts ordinaryFnOnce() -> Futurefactories without caller-side boxing.- Breaking: heap-backed
AsyncScope::runnow borrows&mut selfand is resumable. Usefinish(self)when the cleanup future must consume and own the scope; there is norun_mutalias. - A dependency using
default-features = falsemust enableallocexplicitly to access heap-backed scopes. - Breaking: async scope macros now return the body value and do not let an
inner
returnor?bypass cleanup; internally they consume their scope withfinish. - 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.