Skip to main content

async_await

In Rust, async does NOT mean “run this on another thread.” This is the single most important thing to understand.

Instead:

async means “this function can be paused and resumed without blocking the thread.”

Rust async is about cooperative multitasking, not preemptive multitasking.

Blocking vs async (mental model)

  • Blocking code: let data = socket.read(); // thread is stuck here
  • Async code: let data = socket.read().await; // thread can do other work

When you await, you’re saying:

“If this isn’t ready yet, pause me and let something else run.”

async fn — what it really returns

When you write:

async fn fetch() -> u32 {
42
}

You might think it returns a u32. It does not.

It actually returns:

fn fetch() -> impl Future<Output = u32>

So this:

let x = fetch();

does not run the function
it just creates a Future

Nothing executes until the future is polled (usually by .await).

What is a Future

A Future is basically a state machine.

Simplified definition:

trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

Poll has two states:

enum Poll<T> {
Ready(T),
Pending,
}
  • Ready(value) → computation finished
  • Pending → not ready yet, try again later

Rust async is pull-based:

  • An executor keeps polling futures
  • Futures say “not ready” or “done”

What await actually does

When you write:

let result = some_future.await;

The compiler rewrites this into:

  • A state machine
  • That yields control when the future returns Pending
  • And resumes from the same spot later

Key rule

.await can only appear inside an async context

Because only async functions can be turned into resumable state machines.

Async does not do anything by itself

This code does nothing:

async fn hello() {
println!("hello");
}

fn main() {
hello(); // nothing happens
}

Why? Because:

  • No executor
  • Future is never polled

You need:

  • An async runtime (Tokio, async-std, smol, etc.)

Minimal working async example (Tokio)

Cargo.toml

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

Example 1: Basic async function

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

async fn say_hello() {
println!("hello...");
sleep(Duration::from_secs(1)).await;
println!("world!");
}

#[tokio::main]
async fn main() {
say_hello().await;
}
  1. say_hello() returns a Future
  2. .await polls it
  3. sleep() returns Pending
  4. Tokio pauses say_hello
  5. Timer completes
  6. Tokio resumes say_hello
  7. "world!" prints

The thread was free during the sleep

Async concurrency (not parallelism)

Running multiple async tasks

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

async fn task(id: u32) {
println!("task {} started", id);
sleep(Duration::from_secs(1)).await;
println!("task {} finished", id);
}

#[tokio::main]
async fn main() {
let t1 = task(1);
let t2 = task(2);

tokio::join!(t1, t2);
}

Output (order may vary)

task 1 started
task 2 started
task 1 finished
task 2 finished
  • Both tasks run on one thread
  • They interleave at .await
  • No threads are blocked

Spawning tasks (true async scheduling)

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

#[tokio::main]
async fn main() {
tokio::spawn(async {
sleep(Duration::from_secs(1)).await;
println!("task A");
});

tokio::spawn(async {
sleep(Duration::from_secs(1)).await;
println!("task B");
});

sleep(Duration::from_secs(2)).await;
}
  • spawn schedules independent tasks
  • Executor decides when to poll them
  • Still async, still cooperative

Common misconceptions (very important)

  • Async = multithreading: Nope.
  • Async = faster: Only for IO-bound workloads.
  • await blocks: It yields, not blocks.
  • Async works without runtime: You always need an executor.
  1. When should you use async in Rust?

Use async when:

  • Network IO
  • File IO
  • Timers
  • High concurrency with low CPU usage

Avoid async when:

  • Heavy CPU computation
  • Tight loops
  • Simple synchronous programs

(You can mix async + threads when needed.)