Async Rust powers high-performance systems at companies like Discord, Dropbox, and Cloudflare. This guide covers Tokio best practices for building reliable concurrent systems in 2026.
Basic Async Function
Define async functions with Tokio:
use tokio::fs;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let content = fs::read_to_string("file.txt").await?;
println!("{}", content);
Ok(())
}Common Mistake: Blocking in Async
Never block the async runtime with heavy CPU work:
// BAD: Blocks the worker thread
async fn process_data(data: &Data) {
let result = heavy_computation(data); // Blocks!
db.save(result).await;
}
// GOOD: Spawn to blocking pool
async fn process_data(data: &Data) {
let data = data.clone();
let result = tokio::task::spawn_blocking(move || {
heavy_computation(&data)
}).await??;
db.save(result).await;
}Error Handling with Thiserror
Use thiserror for clean error types:
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ApiError {
#[error("Network error: {0}")]
Network(#[from] reqwest::Error),
#[error("Parse error: {0}")]
Parse(#[from] serde_json::Error),
#[error("Not found: {0}")]
NotFound(String),
}
// Propagate with ?
async fn fetch_user(id: u64) -> Result<User, ApiError> {
let response = client.get(&format!("/users/{}", id)).await?;
if response.status() == 404 {
return Err(ApiError::NotFound(format!("User {}", id)));
}
let user: User = response.json().await?;
Ok(user)
}Structured Concurrency with Tokio
Use tokio::spawn for parallel tasks:
async fn fetch_all_data() -> Result<(User, Posts, Comments), ApiError> {
// Spawn all tasks
let user_handle = tokio::spawn(fetch_user(1));
let posts_handle = tokio::spawn(fetch_posts(1));
let comments_handle = tokio::spawn(fetch_comments(1));
// Await all results
let user = user_handle.await??;
let posts = posts_handle.await??;
let comments = comments_handle.await??;
Ok((user, posts, comments))
}Timeouts: Don't Wait Forever
Always add timeouts to network operations:
use tokio::time::{timeout, Duration};
async fn fetch_with_timeout() -> Result<String, ApiError> {
let result = timeout(
Duration::from_secs(5),
fetch_slow_resource()
).await?;
Ok(result)
}Graceful Shutdown
Handle shutdown signals properly:
#[tokio::main]
async fn main() {
let (tx, rx) = tokio::sync::broadcast::channel(1);
let server = tokio::spawn(server_loop(rx));
let client = tokio::spawn(client_loop());
tokio::signal::ctrl_c().await;
println!("Shutting down...");
tx.send(()).unwrap();
server.abort();
client.abort();
}Best Practices Summary
- Use spawn_blocking for CPU-heavy work
- Add timeouts to all network calls
- Use thiserror for clean errors
- Avoid shared state in async context
- Enable tokio_unstable in config for tracing