Async Basics
9 examples to get you started with Rust async - 6 basic and 3 intermediate.
Prerequisites
- Rust 1.97.0 (edition 2024).
- Add Tokio for runnable examples:
tokio = { version = "1", features = ["full"] }.
Basic Examples
1. async fn and await
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 fndoes not run until.awaited or spawned..awaityields control while waiting for I/O.#[tokio::main]provides the executor for binaries.
Related: Tokio Basics - runtime setup
2. Lazy Futures
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
}- Futures are lazy until polled.
- Dropping a future cancels work that has not completed.
- Store futures in variables only when you intend to await them later.
Related: The Future Trait - poll and wakers
3. Concurrent Tasks with spawn
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::spawnschedules a task on the runtime.JoinHandleis itself a future - await it for the result.- Spawned tasks must be
Sendon the default multi-thread runtime.
Related: Tasks & JoinHandles - task lifecycle
4. Sequential vs Concurrent await
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
}- Sequential
awaitblocks this task until each step finishes. tokio::join!runs futures concurrently on one task.- Use
join!orspawnwhen steps are independent.
Related: Combining Futures - join and select
5. async fn Return Types
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 insideasync fnlike sync functions.- Errors propagate when the future is awaited.
- Use
anyhoworthiserrorin applications for ergonomics.
Related: Cancellation & Timeouts - error paths
6. Blocking Is Forbidden on the Executor
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());
}- Blocking starves Tokio worker threads.
spawn_blockingruns on a dedicated thread pool.- Prefer async crates (
tokio::fs,reqwest) when available.
Related: Blocking in Async - full guide
Intermediate Examples
7. Shared Client Pattern
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());
}- Reuse
reqwest::Clientacross requests (connection pooling). - In Axum, store
ClientinAppStateviaArc. - Close resources on shutdown in lifespan hooks.
Related: Async I/O - network and files
8. Stream of Events (Concept)
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::nextis async iteration.- Backpressure applies to producers and consumers.
- See the Streams page for
futures::Streamin depth.
Related: Streams - async iteration
9. Cancellation by Drop
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
}abortcancels spawned tasks.- Dropping a future without awaiting also cancels.
- Design cleanup with
Droportokio::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+.