Introduction
Rust's async/.await lets you write non-blocking code that reads like synchronous code. The async keyword transforms a function into one that returns an impl Future instead of its declared return type. The .await keyword suspends execution until the future resolves.
Since the 2024 edition, Future and IntoFuture are in the prelude — no imports needed.
Key Concepts
The async keyword transforms a function body into a state machine wrapped in a Future:
rustasync fn fetch_data() -> String { // Returns impl Future<Output = String>, NOT String directly "data".to_string() }
The .await keyword suspends the current future until the awaited one resolves:
rustasync fn process() { let data = fetch_data().await; // Suspend here println!("Got: {data}"); }
Async blocks create anonymous futures that capture their environment:
rustlet my_future = async { let x = fetch_one().await; let y = fetch_two().await; x + y };
Real World Context
Async Rust powers high-performance servers (Axum, Actix), database drivers (sqlx), and HTTP clients (reqwest). Any I/O-bound workload benefits from async.
Deep Dive
Futures in Rust are lazy — nothing happens until you .await or poll them:
rustlet future = fetch_data(); // Future created, NOT started // ... other code ... let data = future.await; // NOW it executes
This is fundamentally different from JavaScript Promises or Go goroutines, which start immediately.\n\nSince Rust 1.85, async closures provide a first-class way to create closures that return futures:\n\nrust\n// async closure — can borrow from captures (unlike || async {})\nlet name = String::from("Alice");\nlet greet = async || {\n println!("Hello, {name}"); // borrows name\n};\ngreet().await;\n\n\nThe AsyncFn, AsyncFnMut, and AsyncFnOnce traits (in the 2024 edition prelude) are the async equivalents of Fn, FnMut, and FnOnce.
Common Pitfalls
- Forgetting
.await— the future never executes and the compiler warns about unusedFuture - Assuming futures run in the background once created — they don't
- Mixing sync and async code without
spawn_blocking
Best Practices
- Always
.awaitor explicitly spawn futures you create - Keep async functions focused on I/O; offload CPU work to blocking threads
- Use
async moveblocks when you need to transfer ownership into the future - Since Rust 1.85, use
async closures(async || {}) when you need closures that return futures and borrow from captures — theAsyncFn,AsyncFnMut, andAsyncFnOncetraits power this
Summary
async creates futures, .await drives them. Futures are lazy and do nothing until polled. The 2024 edition puts Future in the prelude. Async blocks capture their environment like closures. Since Rust 1.85, async closures (async || {}) provide native support for closures that return futures.
Code Examples
// Futures are lazy — nothing runs until awaited
async fn expensive() -> i32 {
println!("Computing..."); // Only prints when awaited
42
}
async fn main_async() {
let future = expensive(); // Nothing printed yet!
println!("Created future");
let result = future.await; // NOW "Computing..." prints
println!("Result: {result}");
}