17 / 20 · Day 6
Day 6 · Concept 17

async / await

Async functions return Futures — values that complete later. .await drives them. Unlike Go or JS, there is no built-in runtime: you pick one (almost always tokio). Cooperative multitasking; cheap tasks; not threads.


1 · The basic shape

rust src/main.rs · tokio main
// Cargo.toml
// tokio = { version = "1", features = ["full"] }

#[tokio::main]
async fn main() {
    let a = fetch("api/a").await;
    let b = fetch("api/b").await;
    println!("{} {}", a, b);
}

async fn fetch(url: &str) -> String {
    // pretend network call
    tokio::time::sleep(std::time::Duration::from_millis(100)).await;
    format!("data from {}", url)
}

async fn returns a Future; .await drives it. #[tokio::main] wraps main in a runtime so you can write async fn main directly.

2 · Run things concurrently

rust src/main.rs · two requests in parallel
#[tokio::main]
async fn main() {
    let (a, b) = tokio::join!(fetch("a"), fetch("b"));
    println!("{} {}", a, b);
}

Sequential .await = wait then wait. tokio::join! = drive both at once. The two requests overlap in time.

3 · Spawn — a task that owns its work

rust src/main.rs
use tokio::task;

#[tokio::main]
async fn main() {
    let h = task::spawn(async {
        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
        "done"
    });

    let val = h.await.unwrap();
    println!("{}", val);
}

tokio::spawn launches an async task on the runtime. The returned JoinHandle resolves to the task's output. Cheap — thousands per second is normal.

4 · async vs threads

std::threadtokio::spawn
OS-level. ~1 MB stack each.User-space. ~few KB.
Pre-emptive scheduling.Cooperative — yields at .await.
Best for CPU-bound work.Best for I/O-bound work.
Can park 100s. Beyond that, expensive.100,000s easily.

5 · Common mistakes

  • Forgetting .await — the future is created but never driven. Compiler warns.
  • Blocking the runtime with sync I/O (std::fs) — use tokio::fs. One thread blocked = many tasks stalled.
  • Using std::sync::Mutex across .await — risks deadlock. Use tokio::sync::Mutex.
  • Holding heavy work in an async fn. CPU loops block the executor. Use tokio::task::spawn_blocking.

6 · When it clicks

  • You reach for async for network and I/O; threads for parallel CPU.
  • Sequential .awaits feel slow — you instinctively use join! or select!.
  • You read "future poll" docs without flinching.
Found this useful?