Skip to content

Async/Await

Rust's async model provides zero-cost abstractions for I/O-bound concurrency. Unlike threads, async tasks are lightweight (no OS thread per task), making them ideal for handling thousands of concurrent connections. The async/await syntax compiles to state machines, requiring a runtime (tokio, async-std) for execution.

Key Facts

  • async fn returns impl Future<Output = T> - lazy, does nothing until polled
  • await suspends execution until the future completes, yielding to the executor
  • No built-in runtime - must choose: tokio (most popular), async-std, smol
  • Futures are state machines generated by the compiler - zero heap allocation for simple cases
  • Async is for I/O-bound work; use threads (spawn_blocking) for CPU-bound work
  • Pin<T> prevents moving self-referential futures after first poll

Tokio Runtime

#[tokio::main]
async fn main() {
    let result = fetch_data("https://api.example.com").await;
    println!("{result}");
}

async fn fetch_data(url: &str) -> String {
    let response = reqwest::get(url).await.unwrap();
    response.text().await.unwrap()
}

Patterns

Concurrent Tasks

use tokio::task;

// Spawn independent tasks
let handle1 = task::spawn(async { fetch_users().await });
let handle2 = task::spawn(async { fetch_orders().await });

// Wait for both
let (users, orders) = tokio::join!(handle1, handle2);

Select (First to Complete)

tokio::select! {
    result = fetch_from_primary() => handle_primary(result),
    result = fetch_from_backup() => handle_backup(result),
    _ = tokio::time::sleep(Duration::from_secs(5)) => timeout(),
}

Streams (Async Iterators)

use tokio_stream::StreamExt;

let mut stream = tokio_stream::iter(vec![1, 2, 3]);
while let Some(value) = stream.next().await {
    println!("{value}");
}

Gotchas

  • Cannot use async in traits directly (stable since Rust 1.75 with RPITIT)
  • Holding a MutexGuard across .await can deadlock - use tokio::sync::Mutex instead of std::sync::Mutex
  • Send bound required for futures spawned with tokio::spawn - all captured data must be Send
  • Async recursion requires Box::pin() to avoid infinite future size

See Also

  • concurrency - thread-based concurrency, Send/Sync traits
  • error handling - async error propagation with ?
  • [[rust-ecosystem]] - tokio, reqwest, and async ecosystem crates