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.
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);
}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.
[dependencies]
tokio = { version = "1", features = ["full"] }#[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.
use tokio::join;
async fn run() {
let (a, b) = join!(fetch_data(), fetch_data()); // both run concurrently
println!("{} {}", a, b);
}