Async Rust with Tokio: A Beginner-Friendly Guide
Async Rust lets one thread handle thousands of network connections. Rust provides async/await syntax; a runtime such as Tokio actually runs the futures.
Setup
[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
Your first async program
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let body = reqwest::get("https://httpbin.org/ip").await?.text().await?;
println!("{body}");
Ok(())
}
Futures are lazy: nothing happens until you .await them (or spawn them).
Run things concurrently
let (users, orders) = tokio::join!(fetch_users(), fetch_orders()); // both at once
// Background tasks
let handle = tokio::spawn(async move {
process_queue().await
});
let result = handle.await?; // JoinHandle
Timeouts
use std::time::Duration;
use tokio::time::timeout;
match timeout(Duration::from_secs(5), fetch_users()).await {
Ok(users) => println!("{} users", users.len()),
Err(_) => eprintln!("timed out"),
}
Channels between tasks
use tokio::sync::mpsc;
let (tx, mut rx) = mpsc::channel::<String>(100);
tokio::spawn(async move {
for i in 0..3 {
tx.send(format!("job {i}")).await.unwrap();
}
});
while let Some(job) = rx.recv().await {
println!("processing {job}");
}
Common pitfalls
- Blocking in async code (heavy CPU work,
std::thread::sleep, blocking file I/O) stalls other tasks. Usetokio::task::spawn_blockingor async equivalents. - Holding a
std::sync::Mutexguard across an.await; usetokio::sync::Mutexor release it first. tokio::spawnneeds'staticfutures, which is whyasync moveandArcappear so often.
Was this article helpful?
Comments
No comments yet. Have a question or a tip? Be the first to share it.