← thecodex.expert · The Codex Family of Knowledge
Rust

Async/Await

An async fn compiles fine with zero dependencies — but calling it does nothing at all until you add an executor crate like Tokio to actually run it.

Rust 1.95+ No built-in runtime Last verified:
Where this bites you

An async fn call that isn't awaited looks exactly like a normal call that ran. It didn't: the call only builds a Future, and a future nobody awaits or polls is dropped without its body ever executing. The compiler only warns (unused_must_use) — the program builds, runs, reports success, and the side effect you wrote never happens.

Rustawait_bug.rs
async fn save_record() {
    println!("record saved to disk");
}

async fn run() {
    save_record();                 // no .await -- just builds a Future and drops it
    println!("run finished");
}
// warning: unused implementer of `Future` that must be used
//   = note: futures do nothing unless you `.await` or poll them
// output: run finished   (the "record saved" line never prints)
Canonical Definition

An async fn returns a value implementing the Future trait rather than running immediately; .await suspends execution until that future resolves, without blocking the underlying thread. Unlike Go or JavaScript, Rust's standard library defines only the language-level async/await syntax and the Future trait — it deliberately ships no executor. Running any async code requires adding a runtime crate, almost always Tokio, which provides the actual scheduler that polls futures to completion.

async fn and .await

An async fn's body doesn't run when called — calling it just produces a Future value. Nothing happens until that future is awaited (inside another async context) or handed to a runtime to poll.

Rustasync_basics.rs
async fn fetch_data() -> String {
    // imagine a network call here
    String::from("data")
}

async fn run() {
    let result = fetch_data().await;   // suspends here until the future resolves
    println!("{}", result);
}

That's the page's own rule — calling an async fn only produces a future — with the consequence spelled out: the body runs only when something awaits or polls it. Adding .await is the entire fix; the same code, awaited, runs the body before the next line:

Rustawait_fix.rs
async fn save_record() {
    println!("record saved to disk");
}

async fn run() {
    save_record().await;           // now the body actually runs
    println!("run finished");
}
// output:
// record saved to disk
// run finished

Adding a runtime: Tokio

Since std has no executor, running async code from main() requires a runtime crate — Tokio's #[tokio::main] macro is the most common way to bootstrap one for a whole program.

RustCargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }
Rustmain.rs
#[tokio::main]
async fn main() {
    let result = fetch_data().await;
    println!("{}", result);
}

Concurrent futures: join!

Awaiting futures one after another runs them sequentially; Tokio's join! macro (or tokio::spawn for true parallel tasks) runs multiple futures concurrently on the same thread, resuming each as it makes progress rather than blocking on one at a time.

Rustconcurrent.rs
use tokio::join;

async fn run() {
    let (a, b) = join!(fetch_data(), fetch_data());   // both run concurrently
    println!("{} {}", a, b);
}

Sources

1
The Rust Project. Asynchronous Programming in Rust (the async book), rust-lang.github.io/async-book, and the Tokio documentation, tokio.rs.