We just saw the official release of Rust's 1.85 version, and a lot of wow changes came with it, one of them would be async closures.
This new feature has caused a lively debate in the Rust community: Are async blocks, a staple of Rust’s async ecosystem, now obsolete? Are they useless?
As of February 28, 2025, the answer seems clear—async blocks are far from dead. Instead, they coexist with async closures, each serving unique purposes in Rust’s ever-evolving toolkit for non-blocking code.
Looking into what this means for developers and why both features remain relevant.
The Basics: Async Blocks vs. Async Closures
To understand what is going on, we first need to understand the two key terms. Async blocks, written as async { ... }
, have been around since Rust’s async/await
syntax debuted in version 1.39
.
They create a single, anonymous future—essentially a one-and-done piece of asynchronous work, just like a normal code block {//code logic}
but with async privileges.
Here’s a simple example:
let future = async {
println!("Running an async block!");
};
future.await;
They’re straightforward, perfect for encapsulating a specific task without reusable structure.
Now, async closures are stabilized in Rust 1.85, with the syntax async |param| { ... }
. These are callable async functions that return a new future each time they’re invoked.
For example:
let closure = async |message: &str| {
println!("Message: {}", message);
};
closure("Hello, Rust!").await;
Unlike async blocks, async closures can take parameters and be reused.
This makes them ideal for scenarios where you want flexibility and callability.
So what’s the big new with them?
Async closures address two long-standing limitations in Rust’s async model.
First, they enable higher-ranked async function signatures, like think passing async functions as arguments to other functions.
Second, they allow futures to borrow from captured variables more naturally, like you with a normal closure, enhancing ergonomics. For example, a situation where you need to process multiple tasks dynamically:
let tasks = vec![
async |url: &str| { println!("Fetching {}", url); },
async |path: &str| { println!("Reading {}", path); },
];
for task in tasks {
task("example.com").await;
}
This kind of reusable, parameterized async logic was difficult before Rust 1.85, but thanks to async closures, it is now fixed.
Are Async Blocks Obsolete?
So, with async closures in the picture, do we still need async blocks? Absolutely. The two features aren’t rivals—they’re teammates. Async blocks shine in simplicity. When you need a one-off future, like fetching data in a single function, they’re the go-to:
async fn fetch_data() -> String {
let response = async {
// Simulate an HTTP request
"Data received".to_string()
}.await;
response
}
No parameters, no reuse, OTOH, and async closures, by contrast, are overkill for such cases but excel in complex scenarios, like event handlers or callbacks requiring repeated execution. So you see, they are just different things, not really enemies.
What does the community think, and what does this mean for you as a developer
The Rust community agrees. Discussions on forums and insights from sources like the Rust Changelogs for 1.85.0 suggest that async blocks remain fundamental. They’re not going anywhere; they’re just sharing the stage with a more versatile variant.
Practical Implications for Developers
For Rust developers, this means more options, not a replacement. Use async blocks for quick, inline async tasks—think of them as the lightweight, single-use tool in your kit.
Reach for async closures when you need reusable async logic, especially in higher-order functions or when passing async behavior around. Here’s a quick guide:
Async Blocks: One-time futures, simple operations (e.g., inline data fetching).
Async Closures: Reusable async functions and parameterized tasks (e.g., dynamic task processing).
Now that you have an idea of the all-new async closures in rust, you might want to update your code base to reflect this beautiful change, increase your readability, and remove those weird async closure walkarounds like wrapping an async block in a rust closure. If you have any issues understanding this, feel free to reach out to me on my LinkedIn if you need more clarification.
P.S. This was what I meant by an async closure walkaround; it was used before the stabilization of async closures.
In the code, you create a normal closure, but the closure's block is async'ed, and a move is added so you do not have borrow issues; instead, all the values used are used once.
let async_closure_walkaround = || async move {};
Play around with this feature so you get a feel for it. See the Rust playground for more practice.