Async Basics
9 examples to get you started with Rust async - 6 basic and 3 intermediate.
Search across all documentation pages
9 examples to get you started with Rust async - 6 basic and 3 intermediate.
tokio = { version = "1", features = ["full"] }.async fn returns a Future that must be driven by a runtime.
async fn fetch_label() -> String {
"ready".to_string()
}
#[tokio::main]
async fn main() {
let label = fetch_label().await;
println!("{label}");
}async fn does not run until .awaited or spawned..await yields control while waiting for I/O.#[tokio::main] provides the executor for binaries.Related: Tokio Basics - runtime setup
Calling an async function creates a future without starting work.
async fn work() {
println!("running");
}
#[tokio::main]
async fn main() {
let _future = work(); // nothing prints yet
work().await; // now it runs
}Related: The Future Trait - poll and wakers
Run work concurrently on the runtime.
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
42
});
let result = handle.await.unwrap();
println!("{result}");
}tokio::spawn schedules a task on the runtime.JoinHandle is itself a future - await it for the result.Send on the default multi-thread runtime.Related: Tasks & JoinHandles - task lifecycle
Two awaits in sequence take longer than concurrent joins.
async fn step(name: &str, ms: u64) {
println!("{name} start");
tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
println!("{name} end");
}
#[tokio::main]
async fn main() {
step("a", 50).await;
step("b", 50).await; // ~100ms total
tokio::join!(step("c", 50), step("d", 50)); // ~50ms total
}await blocks this task until each step finishes.tokio::join! runs futures concurrently on one task.join! or spawn when steps are independent.Related: Combining Futures - join and select
Async functions return impl Future<Output = T>.
async fn ok() -> Result<i32, std::io::Error> {
Ok(1)
}
#[tokio::main]
async fn main() {
match ok().await {
Ok(n) => println!("{n}"),
Err(e) => eprintln!("{e}"),
}
}? works inside async fn like sync functions.anyhow or thiserror in applications for ergonomics.Related: Cancellation & Timeouts - error paths
Never call blocking I/O inside async tasks without offloading.
// BAD in async: std::thread::sleep or std::fs::read_to_string blocking the worker
#[tokio::main]
async fn main() {
let data = tokio::task::spawn_blocking(|| {
std::fs::read_to_string("/etc/hosts")
})
.await
.unwrap()
.unwrap();
println!("{} bytes", data.len());
}spawn_blocking runs on a dedicated thread pool.tokio::fs, reqwest) when available.Related: Blocking in Async - full guide
Open long-lived resources once at startup.
#[tokio::main]
async fn main() {
let client = reqwest::Client::new();
let resp = client
.get("https://httpbin.org/get")
.send()
.await
.unwrap();
println!("{}", resp.status());
}reqwest::Client across requests (connection pooling).Client in AppState via Arc.Related: Async I/O - network and files
Async iteration uses Stream instead of Iterator.
use tokio_stream::{self as stream, StreamExt};
#[tokio::main]
async fn main() {
let mut stream = stream::iter(vec![1, 2, 3]);
while let Some(n) = stream.next().await {
println!("{n}");
}
}StreamExt::next is async iteration.futures::Stream in depth.Related: Streams - async iteration
Dropping a future stops in-flight work at await points.
async fn long_running() {
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
}
#[tokio::main]
async fn main() {
let handle = tokio::spawn(long_running());
handle.abort();
let _ = handle.await; // JoinError expected
}abort cancels spawned tasks.Drop or tokio::select! for graceful shutdown.Related: Graceful Shutdown - production patterns
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.
Reviewed by Chris St. John·Last updated Jul 16, 2026