Why Drop Can't Be Async
Let's start slowly, with the actual problem, before we look at any solution.
In Rust, when a value goes out of scope, its Drop::drop method runs automatically:
struct Connection {
// ...
}
impl Drop for Connection {
fn drop(&mut self) {
// clean up here
}
}
This is one of Rust's nicest features — you don't have to remember to close things, the compiler does it for you, deterministically, at a well-defined point.
The trouble is that fn drop(&mut self) is an ordinary, synchronous function. It cannot
contain .await. So if closing your resource properly requires talking to something
asynchronously — sending a "goodbye" message over a socket, flushing a batch of buffered log
lines to a remote collector, releasing a lease held in a database — you're stuck. There is no
async fn drop.
The usual workarounds, and why they're not quite enough
A few patterns show up in real codebases to work around this:
- Block inside
drop. Callfutures::executor::block_on(self.cleanup())right there indrop. This works, but it blocks whatever thread happens to be dropping the value — which, if you're inside an async runtime already, can deadlock or badly stall other tasks. - Fire off an unstructured
tokio::spawnand hope. This detaches the cleanup from anything that would wait for it. If your program exits (or the runtime shuts down) before that spawned task gets scheduled, the cleanup silently never happens. - Require callers to remember an explicit
.close().await. This is the "correct" pattern in the sense that it actually works, but it's easy to forget, and the whole point ofDropis that you shouldn't have to remember.
And there's a second problem hiding underneath all three: none of them account for what happens when many values need this kind of cleanup at once — say, a burst of a few thousand connections closing during a deploy. Spawn all of them unbounded, and you can hammer whatever they're talking to. Block on each one in turn, and cleanup that should take milliseconds takes minutes.
Prior art
redrop isn't the first attempt at this. A few existing crates tackle the same problem:
async-dropperoffers anAsyncDroptrait, with two ways to use it: a derive macro (the type must implementDefaultandPartialEq, and the drop fires when the value differs from its default), or anAsyncDropper<T>wrapper that owns your value and spawns a detached future to clean it up.async_dropis smaller and similarly named: anAsyncDroptrait returning a boxed future, plus aDropper<T>wrapper that calls it when dropped.- The manual
tokio::spawnidiom. Before crates like these existed — and still often today — people hand-roll this: implementDropsynchronously, pull the resource handles you need out of&mut self, andtokio::spawna task to close them, since you can't.awaitinsidedropitself.
These all solve the single-value case reasonably well. What they don't address is the second
problem above: many drops firing at once, with no shared queue, no backpressure, and no way to
know cleanup actually finished before the process exits. redrop takes the same starting point —
you still can't await inside drop, so cleanup still has to be handed off — but routes every
#[async_drop] through one shared collector, so a burst of drops is queued and drained under a
bounded concurrency limit instead of spawned unbounded, and redrop::shutdown() gives you an
explicit .await point to guarantee the queue is drained before exit.
Rust itself is working on closing this gap at the language level, too. Nightly has an experimental
AsyncDrop trait behind the
#![feature(async_drop)] gate, tracked in
rust-lang/rust#126482 and described by the
async fundamentals initiative's async drop
roadmap. It's
the real, compiler-integrated version of what this crate is approximating — an actual
async fn drop. It's also unstable, with no stabilization date, and existing code needs something
that works on stable Rust today. redrop is meant as a bridge: a proc-macro #[async_drop] that
gets you most of the ergonomics now, so that migrating to the language feature later — if and when
it stabilizes with a similar shape — should mean deleting the macro, not rewriting the cleanup
logic.
What we actually want
We want something that:
- Lets us write the cleanup as a normal
async fn, close to the type it belongs to. - Runs automatically when the value is dropped — no explicit call to remember.
- Doesn't block the thread that triggered the drop.
- Handles a burst of many drops without either losing them or overwhelming whatever they talk to.
That's what the rest of this tutorial builds, using redrop's #[async_drop] macro.