Introduction
Just as synchronous closures implement Fn, FnMut, or FnOnce, async closures implement AsyncFn, AsyncFnMut, or AsyncFnOnce. These traits were stabilized in Rust 1.85 and added to the prelude, so they are available without any imports. Understanding these bounds is essential for writing generic async APIs.
Key Concepts
AsyncFn: An async closure that only borrows captures immutably. Can be called multiple times concurrently.AsyncFnMut: An async closure that may mutate captures. Can be called multiple times but not concurrently.AsyncFnOnce: An async closure that may consume captures. Can be called at most once.- Trait hierarchy:
AsyncFn⊂AsyncFnMut⊂AsyncFnOnce, mirroring the sync hierarchy.
Real World Context
Web frameworks need to accept async handlers generically. Axum's handler system, for instance, requires closures that can be called for each request. With AsyncFn bounds, you can write middleware and handler registration APIs that accept async closures directly, without requiring users to wrap their logic in boxed futures.
Deep Dive
Using AsyncFn bounds
To accept an async closure in a generic function, use AsyncFn bounds:
rust// Accepts any async closure that takes a &str and returns a String async fn process_with<F>(handler: F, input: &str) -> String where F: AsyncFn(&str) -> String, { handler(input).await } // Usage let result = process_with( async |name: &str| format!("Hello, {name}!"), "Alice", ).await; assert_eq!(result, "Hello, Alice!");
The bound AsyncFn(&str) -> String means: "a callable that takes &str, returns a Future<Output = String>, and only borrows captures immutably."
AsyncFnMut for stateful handlers
When the closure needs to mutate state between calls:
rustasync fn call_n_times<F>(mut handler: F, times: u32) where F: AsyncFnMut() -> (), { for _ in 0..times { handler().await; } } let mut counter = 0; call_n_times(async || { counter += 1; println!("Call #{counter}"); }, 3).await;
Note the mut handler parameter — just like FnMut, you need mutable access to call an AsyncFnMut.
AsyncFnOnce for consuming operations
Some operations should only happen once — database migrations, one-time initialization:
rustasync fn run_once<F, T>(setup: F) -> T where F: AsyncFnOnce() -> T, { setup().await } let db_pool = create_pool().await; let migration_result = run_once(async || { // Consumes db_pool — can only run once run_migrations(db_pool).await }).await;
Comparing with the old approach
Before AsyncFn traits, you had to use complex bounds:
rust// Old approach (pre-1.85) async fn old_process<F, Fut>(handler: F, input: String) -> String where F: Fn(String) -> Fut, Fut: Future<Output = String>, { handler(input).await } // New approach (1.85+) async fn new_process<F>(handler: F, input: String) -> String where F: AsyncFn(String) -> String, { handler(input).await }
The new syntax is more concise and correctly handles the relationship between the closure and its returned future.
Common Pitfalls
- Using
AsyncFnwhenAsyncFnOncesuffices — If you only call the closure once, useAsyncFnOncefor maximum flexibility.AsyncFnunnecessarily restricts to immutable captures. - Trying to call
AsyncFnMutconcurrently — UnlikeAsyncFn,AsyncFnMutclosures cannot be called concurrently because they require mutable access. - Mixing old and new syntax — Avoid using
Fn() -> impl Future<Output = T>bounds whenAsyncFn() -> Tis available. The new bounds handle lifetimes more correctly.
Best Practices
- Use the weakest bound needed —
AsyncFnOnce>AsyncFnMut>AsyncFnin terms of flexibility for callers. - Prefer
impl AsyncFnin argument position — For simple cases,handler: impl AsyncFn(Args) -> Outputreads cleanly. - Migrate old
Fn() -> Futurebounds — When updating to Rust 1.85+, replace two-generic patterns with singleAsyncFnbounds.
Summary
AsyncFn,AsyncFnMut,AsyncFnOnceare the async equivalents ofFn,FnMut,FnOnce.- They are in the prelude — no imports needed.
- Use the weakest bound (
AsyncFnOnce) unless you need repeated calls. - The new traits simplify generic async APIs by eliminating the
F: Fn() -> Fut, Fut: Futurepattern. - The hierarchy mirrors sync closures:
AsyncFn⊂AsyncFnMut⊂AsyncFnOnce.
Code Examples
// Middleware pattern using AsyncFn
async fn timed_execution<F, T>(
label: &str,
handler: F,
) -> T
where
F: AsyncFn() -> T,
{
let start = std::time::Instant::now();
let result = handler().await;
let elapsed = start.elapsed();
println!("{label} completed in {elapsed:?}");
result
}
// Old vs New comparison
// Old: F: Fn(Request) -> Fut, Fut: Future<Output = Response>
// New: F: AsyncFn(Request) -> Response
async fn handle_request<F>(handler: F, request: Request) -> Response
where
F: AsyncFn(Request) -> Response,
{
timed_execution("Request", async || handler(request).await).await
}