Rust Async Await

Many programs spend most of their time waiting: for a web server reply, a file read, or a database answer. Threads handle waiting by sleeping, yet thousands of sleeping threads consume large amounts of memory. Asynchronous programming offers a lighter approach. Think of a restaurant waiter who takes an order, moves to the next table while the kitchen cooks, and returns when the food is ready.

Blocking and Non-Blocking Work

 BLOCKING (one waiter per table)
 Waiter 1: take order ... wait ... serve
 Waiter 2: take order ... wait ... serve

 ASYNC (one waiter, many tables)
 Waiter: order A -> order B -> serve A -> order C -> serve B ...

Async code lets one thread juggle many waiting tasks.

The async Keyword

Placing async before a function turns it into a function that returns a future. A future describes work that will finish later. Calling the function does not run the body.

async fn greet() -> String {
    String::from("Hello from async")
}

The return type is no longer String. The call produces a value that promises a String eventually.

The await Keyword

The .await keyword pauses the current task until a future completes. While the task waits, the runtime switches to other ready tasks.

async fn show() {
    let text = greet().await;
    println!("{text}");
}

The keyword works only inside async functions and blocks.

Futures Are Lazy

A future does nothing until something polls it. Creating a future without awaiting it wastes the effort, because the work never starts. Treat a future like a recipe card: reading the card does not cook the meal.

Why a Runtime Is Needed

The standard library defines futures yet ships no executor, the engine that drives futures to completion. Programs choose a runtime crate. The most common choice is tokio.

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

A Working Program

The attribute #[tokio::main] sets up the runtime and lets main be async.

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

async fn fetch_data(id: u32) -> String {
    sleep(Duration::from_millis(500)).await;
    format!("data {id}")
}

#[tokio::main]
async fn main() {
    let result = fetch_data(1).await;
    println!("{result}");
}
data 1

Running Tasks Together

Awaiting tasks one after another still runs them in sequence. The tokio::join! macro starts several futures at once and waits for all of them.

#[tokio::main]
async fn main() {
    let (a, b, c) = tokio::join!(
        fetch_data(1),
        fetch_data(2),
        fetch_data(3),
    );
    println!("{a}, {b}, {c}");
}
 Sequential:  |--1--|--2--|--3--|     1.5 seconds
 Joined:      |--1--|
              |--2--|
              |--3--|                0.5 seconds

Spawning Background Tasks

The tokio::spawn function launches a task that runs independently. The returned handle lets you collect the result later.

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        fetch_data(7).await
    });

    let output = handle.await.unwrap();
    println!("{output}");
}

Async Compared with Threads

FeatureThreadsAsync Tasks
Memory per unitLargeVery small
Best forHeavy calculationWaiting on input and output
SwitchingOperating system decidesTasks yield at await points
Typical countDozensThousands or more

Common Pitfalls

  • Calling a slow blocking function inside an async task freezes other tasks. Use async versions such as tokio::time::sleep instead of std::thread::sleep.
  • Forgetting .await leaves the work unstarted, and the compiler emits a warning.
  • Holding a standard Mutex guard across an await point can block the runtime. Use tokio::sync::Mutex in that case.

Where Async Shines

  • Web servers handling many connections
  • Programs that call several network services
  • Chat applications and live data feeds
  • Tools that read many files or sockets concurrently

Heavy math on large data fits threads better, because async tasks yield only at await points.

Practice Ideas

  • Write three async functions that sleep for different times and run them together with join!.
  • Time sequential awaits against joined awaits using std::time::Instant.
  • Spawn five tasks and collect their return values in a vector.

Key Points

  • async fn returns a future, and .await waits for its result.
  • Futures stay idle until a runtime drives them.
  • tokio supplies a popular runtime and helper tools.
  • Use join! or spawn to run tasks together.
  • Avoid blocking calls inside async code.