RustArena

Async/await in Rust with Tokio: the basics

Getting started with async Rust: futures, async fn and .await, #[tokio::main], tokio::spawn, join!, async sleep, and when to use async vs threads.

Async Rust lets one thread juggle many tasks that spend most of their time waiting: on the network, a database, a timer. While one task waits, the thread runs another. That's why web servers written with async Rust handle tens of thousands of connections with a handful of threads.

Futures are lazy

An async fn doesn't run when you call it. It returns a future, a value representing work that hasn't happened yet. .await runs the future to completion (pausing the current task while it waits) and gives you its output:

async fn fetch_price(item: &str) -> u32 { ... }

let price = fetch_price("potion").await;

Calling an async function without awaiting it does nothing, and rustc warns: "unused implementer of Future that must be used ... futures do nothing unless you .await or poll them".

.await is only allowed inside async code:

async fn fetch() -> u32 {
    5
}

fn main() {
    let n = fetch().await;
    println!("{n}");
}
error[E0728]: `await` is only allowed inside `async` functions and blocks

A runtime: Tokio

Rust's standard library defines futures but doesn't include a runtime to drive them. Tokio is the one most teams use. Add it to Cargo.toml:

[dependencies]
tokio = { version = "1", features = ["full"] }

and mark main:

#[tokio::main]
async fn main() {
    let price = fetch_price("potion").await;
    println!("{price}");
}

#[tokio::main] starts a multi-threaded runtime and runs your async main on it.

Running things concurrently

Awaiting one future after another runs them in sequence. To run them at the same time:

Don't block the runtime

Inside async code, never call blocking functions such as std::thread::sleep, heavy CPU loops, or blocking file and network I/O: they freeze a whole worker thread and every task on it. Use the async versions (tokio::time::sleep, tokio::fs, tokio::net), and move CPU-heavy or blocking work to tokio::task::spawn_blocking.

Async vs threads

Use async when Use threads when
many tasks mostly waiting on I/O (servers, crawlers, chat) a few CPU-heavy jobs (image processing, compression)
you need timeouts and cancellation everywhere the code calls blocking libraries
you use async libraries (axum, reqwest, sqlx) simplicity matters more than scale

For CPU-bound data parallelism, rayon is usually simpler than both.

Common mistakes

Example

use std::time::Duration;
use tokio::time::sleep;

async fn fetch_price(item: &str, delay_ms: u64) -> u32 {
    sleep(Duration::from_millis(delay_ms)).await; // pretend this is a network call
    match item {
        "potion" => 30,
        "sword" => 150,
        _ => 0,
    }
}

async fn checkout(items: Vec<&'static str>) -> Result<u32, String> {
    let mut total = 0;
    for item in items {
        let price = fetch_price(item, 5).await;
        if price == 0 {
            return Err(format!("unknown item {item}"));
        }
        total += price;
    }
    Ok(total)
}

#[tokio::main]
async fn main() {
    // Concurrently: both waits overlap. Results come back in argument order.
    let (potion, sword) = tokio::join!(fetch_price("potion", 30), fetch_price("sword", 10));
    println!("potion {potion}, sword {sword}");

    // Independent tasks on the runtime; await the handles in order.
    let mut handles = Vec::new();
    for (i, cart) in [vec!["potion", "sword"], vec!["potion", "shield"], vec!["sword"]].into_iter().enumerate() {
        handles.push(tokio::spawn(async move { (i, checkout(cart).await) }));
    }
    for h in handles {
        let (i, result) = h.await.expect("task panicked");
        println!("cart {i}: {result:?}");
    }

    // CPU-heavy work goes to a blocking thread, not the async workers.
    let sum = tokio::task::spawn_blocking(|| (1..=1_000_000u64).sum::<u64>()).await.unwrap();
    println!("sum {sum}");
}

Output:

potion 30, sword 150
cart 0: Ok(180)
cart 1: Err("unknown item shield")
cart 2: Ok(150)
sum 500000500000

Practice it