Async Closures - Language-Version Considerations
An async closure—written async |args| { ... } or async move |args| { ... } (stabilized in Rust 1.85 alongside the Rust 2024 edition)—is a closure that returns a future allowed to borrow directly from the closure's own captured environment across .await points, governed by the AsyncFn, AsyncFnMut, and AsyncFnOnce traits in the standard prelude.
Why Ordinary |x| async { ... } Closures Could Not Borrow Captures Mutably
Prior to Rust 1.85, writing |x| async { ... } created a regular synchronous closure (Fn / FnMut) whose return type happened to be an async block (F: FnMut(Arg) -> Fut, Fut: Future).
However, in FnMut::call_mut(&mut self, args) -> Self::Output, the return type Self::Output (Fut) cannot borrow from &mut self because FnMut's signature has no Generic Associated Type lifetime tying Self::Output to &'a mut self! Attempting to return an async block that borrowed a captured variable from a Fn or FnMut closure failed to compile:
1. Pre-1.85 Workaround (`|x| async move { ... }` with `F: Fn(Arg) -> Fut`):
`Fn::call(&self)` returns `Fut` (which must be independent of `&self`!).
---> Forced cloning `Arc<T>` on every call or could not borrow `&mut` captures!
2. Native Async Closure (`async |x| { ... }` with `AsyncFnMut` — Rust 1.85+ / 2024):
Returned future is a Lending GAT `CallRefFuture<'a>` tied to `&'a mut self`!
---> Can borrow and mutate captured local variables across `.await` points!
Evolution of Async Closure Support Across Rust Versions
| Rust Version | Syntax & Trait Bound | Capabilities & Limitations |
|---|---|---|
| Rust 1.39 – 1.84 | |x| async move { ... } bounded by F: FnMut(A) -> Fut, Fut: Future<Output = R> |
Returned future Fut cannot borrow from the closure's captured state (requires Arc::clone per call) |
| Rust 1.85+ (Rust 2024 Edition) | async |x| { ... } bounded by F: AsyncFn(A) -> R / AsyncFnMut(A) -> R / AsyncFnOnce(A) -> R |
Returned future can borrow &self or &mut self captures across .await; single trait bound (no separate Fut type parameter needed!) |
Using Native async || Closures with AsyncFnMut
With AsyncFn, AsyncFnMut, and AsyncFnOnce in the standard prelude, higher-order async functions take a single trait bound F: AsyncFnMut(T) -> U, and the async || closure can mutate captured local state across .await calls without Arc or Mutex:
use std::future::Future;
use std::pin::pin;
use std::task::{Context, Poll, Waker};
async fn run_for_each<T, F>(items: &[T], mut callback: F)
where
T: Copy,
F: AsyncFnMut(T),
{
for &item in items {
callback(item).await;
}
}
fn main() {
let fut = async {
// Captured local `Vec` mutated directly inside `async |n|` across `.await`!
let mut history = Vec::new();
run_for_each(&[10, 20, 30], async |n| {
std::future::ready(()).await;
history.push(n * 2);
})
.await;
history
};
let mut pinned = pin!(fut);
let mut cx = Context::from_waker(Waker::noop());
println!("async closure history: {:?}", pinned.as_mut().poll(&mut cx));
}
async closure history: Ready([20, 40, 60])