← Back to Rust series
🦀
Modern Rust
Modern · Prerequisite: lesson 21

22. async/await + tokio intro

async/await is the async programming model for handling many I/O-bound tasks efficiently. Rust ships the syntax but not a runtime — you pick one, and tokio is the de-facto standard. This lesson covers Futures, async fn, .await, the tokio runtime entry point, and join! for concurrent tasks.

RustasyncawaittokioFutureasync I/O
Duration
~2 hours
Level
📊 Intermediate
Prerequisite
🎯 Lesson 21
OUTCOME
async/await is the async programming model for handling many I/O-bound tasks efficiently. Rust ships the syntax but not a runtime — you pick one, and tokio is the de-facto standard. This lesson covers Futures, async fn, .await, the tokio runtime entry point, and join! for concurrent tasks.

What you'll learn

  • 1Define async functions with `async fn`
  • 2Wait for completion with `.await`
  • 3Use `#[tokio::main]` to make main an async entry point
  • 4Run several tasks concurrently with `join!` / `try_join!`
  • 5Tell concurrency from parallelism

Overview

OS threads are heavy (multi-MB stacks each) — thousands of them sink a system. Async lets one thread host thousands of tasks; tasks waiting on I/O yield the CPU automatically. Web servers, DB clients, and network tools are the typical use cases.

Core Concepts

1) Future — a value that isn't ready yet

Every `async fn` returns `impl Future<Output = T>`. The runtime polls it and gets Pending (not yet) or Ready(T) (done).

2) .await — wait for completion

Adding `.await` to a Future **suspends** the current task until the value is ready, freeing the runtime to drive other tasks. This is what makes async code look almost like sync.

3) Pick a runtime — tokio

Rust's async is just syntax; the actual execution engine lives outside the standard library. **tokio** is the practical standard — multi-threaded worker pool + I/O polling.

4) Concurrency vs. parallelism

TermMeaning
ConcurrencyMany tasks alternating (works on one thread too)
ParallelismMany tasks physically simultaneous (multiple cores)

Async is concurrency-first. tokio's multi-thread runtime adds parallelism on top.

Hands-on Examples

Cargo.toml:

toml
[dependencies]
tokio = { version = "1", features = ["full"] }
rust
use tokio::time::{sleep, Duration};

async fn say_after(ms: u64, msg: &str) {
    sleep(Duration::from_millis(ms)).await;
    println!("{}", msg);
}

#[tokio::main]
async fn main() {
    say_after(100, "hello").await;
    say_after(50, "world").await;
    // Output: hello → world (sequential await)
}

Concurrent progress with join!:

rust
use tokio::time::{sleep, Duration};

async fn task(name: &str, ms: u64) {
    sleep(Duration::from_millis(ms)).await;
    println!("{} done after {}ms", name, ms);
}

#[tokio::main]
async fn main() {
    tokio::join!(
        task("A", 100),
        task("B", 50),
        task("C", 80),
    );
    // Order: B (50ms) → C (80ms) → A (100ms); whole thing in ~100ms
}

Async + Result + `?`:

rust
use std::error::Error;
use tokio::fs;

async fn read_file(path: &str) -> Result<String, Box<dyn Error>> {
    let s = fs::read_to_string(path).await?;
    Ok(s)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let s = read_file("data.txt").await?;
    println!("{} bytes", s.len());
    Ok(())
}

Common Mistakes

Q. Calling an async fn does nothing — why?

A. Calling it just **creates a Future**; nothing runs. Either `.await` it or hand it to `tokio::spawn` to register with the runtime.

Q. tokio or async-std?

A. **tokio** is effectively standard. The biggest ecosystem (axum / reqwest / sqlx / ...) is built on it.

Q. Can I `std::thread::sleep` inside async?

A. **No.** That blocks the entire thread and stalls every other task on it. Use `tokio::time::sleep(...).await`.

Q. Can I hold a non-Send type across an await?

A. On tokio's multi-threaded runtime any value alive across an await must be Send. If not, the compiler will tell you.

Recap

  • async fn returns `impl Future<Output = T>`
  • .await suspends, freeing the runtime to drive others
  • tokio is the de-facto runtime — enter via #[tokio::main]
  • join! / try_join! run multiple Futures concurrently
  • Concurrency ≠ parallelism; async wins big on I/O bound work

Try It Yourself

  1. Three tasks sleeping 100ms / 50ms / 200ms — wrap them in join! and verify total wall-clock is ~200ms
  2. Read two files concurrently with tokio::fs::read_to_string and sum the lengths
  3. Add reqwest to Cargo.toml and write an async fn that GETs a URL, propagating errors with `?`
Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub ↗