← Back to BlogArticle

Async Rust Best Practices: Tokio in 2026

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