Rust async/await
debt(d6/e5/b7/t7)
Closest to 'specialist tool catches it' (d5), bumped to d6: clippy catches some patterns (unused futures via must_use, MutexGuard-across-await detection) but the worst failures — forgotten .await, missing runtime, sequential awaits that should be concurrent — surface only at runtime as hangs or never-run code, leaning toward 'silent until users hit it'.
Closest to 'touches multiple files / significant refactor' (e5). The quick_fix lists several distinct corrections (add runtime, ensure futures awaited/spawned, restructure with join!), and fixing Send errors from held guards or restructuring blocking work into spawn_blocking can require reworking how a component handles concurrency, beyond a one-line swap.
Closest to 'strong gravitational pull' (b7): async colours every function signature it touches (async fn propagates up the call stack), applies across web, cli, queue-worker and library contexts, and the choice of runtime (tokio) becomes load-bearing across the codebase shaping every I/O-touching change.
Closest to 'serious trap' (t7): the misconception that calling an async fn starts running it like a background thread is exactly backwards — futures are lazy and do nothing until awaited or spawned, contradicting how async behaves in most other languages (JS/C# eager promises/tasks), making the obvious mental model wrong.
Also Known As
TL;DR
Explanation
Rust's async/await lets you write asynchronous code that reads like sequential code while compiling down to efficient state machines. An `async fn` or `async` block returns a value implementing the `Future` trait. A future is lazy: it does nothing until polled. The `.await` operator suspends the current future until the awaited future is ready, yielding control back to the executor so other tasks can make progress on the same thread.
Unlike threads, futures are zero-cost abstractions with no inherent runtime. Rust's standard library defines the `Future` trait and the language provides syntax, but it deliberately ships no executor. You must bring a runtime such as Tokio or async-std to actually drive futures to completion by repeatedly calling `poll`. The runtime also supplies async I/O primitives, timers, and task spawning.
A key mental model: awaiting does not spawn concurrency by itself. `let a = fetch().await; let b = fetch().await;` runs sequentially. To run futures concurrently you combine them with `join!`, `select!`, or spawn separate tasks. Because the compiler transforms async functions into state machines that capture locals across await points, borrowed references held across `.await` must satisfy lifetime and `Send` requirements, which is why some types like `MutexGuard` from `std::sync` cannot be held across awaits in multithreaded runtimes.
Error handling uses the normal `Result` and `?` operator inside async functions. Cancellation happens by dropping a future, so any cleanup must be drop-safe rather than relying on code after an await that may never run. Pinning (`Pin`) appears because self-referential state machines must not move in memory once polled; most users rely on `Box::pin` or runtime helpers rather than manipulating `Pin` directly. Mastering async/await means understanding laziness, the missing runtime, concurrent combinators, and the `Send`/lifetime constraints the borrow checker enforces across suspension points.
Common Misconception
Why It Matters
Common Mistakes
- Calling an async fn and forgetting to .await or spawn it, so the future never executes.
- Awaiting futures sequentially when they could run concurrently with join! or tokio::spawn.
- Holding a std::sync::MutexGuard across an .await, causing Send errors or deadlocks under a multithreaded executor.
- Doing CPU-heavy blocking work inside an async task and starving the executor instead of using spawn_blocking.
- Assuming code after an .await always runs, ignoring that dropping a future cancels it mid-flight.
Avoid When
- The workload is purely CPU-bound with no I/O, where threads or rayon are simpler and faster.
- A small synchronous CLI tool where adding a runtime is unnecessary complexity.
- You only need a single blocking network call and the extra async machinery adds no value.
When To Use
- Handling many concurrent I/O-bound connections such as HTTP servers or database clients.
- Building network services where blocking a thread per request would not scale.
- Composing multiple independent I/O operations to run concurrently with join! or select!.
Code Examples
use std::sync::Mutex;
use std::time::Duration;
async fn fetch(id: u32) -> u32 { id * 2 }
async fn run(state: &Mutex<u32>) {
// Sequential awaits: these run one after another, not concurrently.
let a = fetch(1).await;
let b = fetch(2).await;
// Holding a std::sync guard across .await -> not Send, can deadlock.
let mut g = state.lock().unwrap();
tokio::time::sleep(Duration::from_millis(10)).await;
*g += a + b;
}
fn main() {
let state = Mutex::new(0);
// No runtime: this future is never polled, so run() never executes.
let _ = run(&state);
}
use tokio::sync::Mutex;
use tokio::join;
use std::time::Duration;
async fn fetch(id: u32) -> u32 { id * 2 }
async fn run(state: &Mutex<u32>) {
// Run independent futures concurrently.
let (a, b) = join!(fetch(1), fetch(2));
// Async-aware lock can be held across .await safely.
let mut g = state.lock().await;
tokio::time::sleep(Duration::from_millis(10)).await;
*g += a + b;
}
#[tokio::main]
async fn main() {
let state = Mutex::new(0);
run(&state).await; // future is actually driven to completion
println!("{}", *state.lock().await);
}