How do I define async methods in traits?

Walkthrough

Rust doesn't natively support async functions in traits (though this is changing with recent versions). The async-trait crate provides a macro that enables async methods in trait definitions. It works by boxing the futures returned by async methods, making them dynamically dispatched.

Why you need async-trait:

  1. Traits with async methods require special handling—the compiler needs to know the future's size
  2. async fn in traits returns impl Future, which has an anonymous type
  3. The macro converts async fn to return Pin<Box<dyn Future>>, which has a known size
  4. Essential for building async abstractions, plugins, or middleware systems

Note: Rust 1.75+ supports native async fn in traits for some cases, but async-trait remains useful for object-safe traits and complex scenarios.

Code Example

# Cargo.toml
[dependencies]
async-trait = "0.1"
tokio = { version = "1", features = ["full"] }
use async_trait::async_trait;
use std::error::Error;
 
// ===== Basic Async Trait =====
 
#[async_trait]
pub trait Database {
    async fn get_user(&self, id: u64) -> Option<String>;
    async fn save_user(&self, id: u64, name: &str) -> Result<(), Box<dyn Error>>;
}
 
// ===== Implementation for Concrete Type =====
 
struct InMemoryDb {
    users: std::collections::HashMap<u64, String>,
}
 
impl InMemoryDb {
    fn new() -> Self {
        let mut users = std::collections::HashMap::new();
        users.insert(1, "Alice".to_string());
        users.insert(2, "Bob".to_string());
        Self { users }
    }
}
 
#[async_trait]
impl Database for InMemoryDb {
    async fn get_user(&self, id: u64) -> Option<String> {
        // Simulate async work
        tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
        self.users.get(&id).cloned()
    }
    
    async fn save_user(&self, id: u64, name: &str) -> Result<(), Box<dyn Error>> {
        tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
        self.users.insert(id, name.to_string());
        Ok(())
    }
}
 
// ===== Multiple Implementations =====
 
struct FileDb {
    path: String,
}
 
#[async_trait]
impl Database for FileDb {
    async fn get_user(&self, id: u64) -> Option<String> {
        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
        // Simulated file read
        println!("Reading user {} from {}", id, self.path);
        Some(format!("user_{}", id))
    }
    
    async fn save_user(&self, id: u64, name: &str) -> Result<(), Box<dyn Error>> {
        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
        println!("Saving user {}={} to {}", id, name, self.path);
        Ok(())
    }
}
 
// ===== Using Trait Objects (dyn Trait) =====
 
async fn fetch_user(db: &dyn Database, id: u64) -> Option<String> {
    db.get_user(id).await
}
 
async fn process_users(databases: &[Box<dyn Database>]) {
    for (i, db) in databases.iter().enumerate() {
        if let Some(user) = db.get_user(1).await {
            println!("Database {} found user: {}", i, user);
        }
    }
}
 
// ===== Async Trait with Self Bounds =====
 
#[async_trait]
pub trait Cache: Send + Sync {
    async fn get(&self, key: &str) -> Option<String>;
    async fn set(&self, key: &str, value: &str);
    async fn clear(&self);
}
 
struct RedisCache {
    url: String,
}
 
#[async_trait]
impl Cache for RedisCache {
    async fn get(&self, key: &str) -> Option<String> {
        println!("GET {} from Redis at {}", key, self.url);
        None
    }
    
    async fn set(&self, key: &str, value: &str) {
        println!("SET {}={} in Redis at {}", key, value, self.url);
    }
    
    async fn clear(&self) {
        println!("CLEAR all keys in Redis at {}", self.url);
    }
}
 
// ===== Async Trait with Generics =====
 
#[async_trait]
pub trait Repository<T> {
    async fn find_by_id(&self, id: u64) -> Option<T>;
    async fn save(&self, entity: T) -> Result<(), Box<dyn Error>>;
    async fn delete(&self, id: u64) -> bool;
}
 
struct User {
    id: u64,
    name: String,
}
 
struct UserRepository {
    cache: Box<dyn Cache>,
}
 
#[async_trait]
impl Repository<User> for UserRepository {
    async fn find_by_id(&self, id: u64) -> Option<User> {
        let cached = self.cache.get(&format!("user:{}", id)).await;
        cached.map(|name| User { id, name })
    }
    
    async fn save(&self, entity: User) -> Result<(), Box<dyn Error>> {
        self.cache.set(&format!("user:{}", entity.id), &entity.name).await;
        Ok(())
    }
    
    async fn delete(&self, id: u64) -> bool {
        println!("Deleting user {}", id);
        true
    }
}
 
// ===== Real-World Pattern: Plugin System =====
 
#[async_trait]
pub trait Plugin {
    fn name(&self) -> &str;
    async fn initialize(&mut self) -> Result<(), Box<dyn Error>>;
    async fn execute(&self, input: &str) -> Result<String, Box<dyn Error>>;
    async fn shutdown(&mut self) -> Result<(), Box<dyn Error>>;
}
 
struct LoggingPlugin;
 
#[async_trait]
impl Plugin for LoggingPlugin {
    fn name(&self) -> &str {
        "LoggingPlugin"
    }
    
    async fn initialize(&mut self) -> Result<(), Box<dyn Error>> {
        println!("Initializing {}", self.name());
        Ok(())
    }
    
    async fn execute(&self, input: &str) -> Result<String, Box<dyn Error>> {
        println!("[LOG] {}", input);
        Ok(format!("Logged: {}", input))
    }
    
    async fn shutdown(&mut self) -> Result<(), Box<dyn Error>> {
        println!("Shutting down {}", self.name());
        Ok(())
    }
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    // Use concrete implementation
    let db = InMemoryDb::new();
    
    if let Some(user) = db.get_user(1).await {
        println!("Found user: {}", user);
    }
    
    db.save_user(3, "Charlie").await?;
    println!("Saved user 3");
    
    // Use through trait object
    let db: Box<dyn Database> = Box::new(InMemoryDb::new());
    let user = fetch_user(&*db, 2).await;
    println!("Fetched via trait object: {:?}", user);
    
    // Multiple implementations
    let databases: Vec<Box<dyn Database>> = vec![
        Box::new(InMemoryDb::new()),
        Box::new(FileDb { path: "/data/db.json".to_string() }),
    ];
    process_users(&databases).await;
    
    // Plugin system
    let mut plugins: Vec<Box<dyn Plugin>> = vec![
        Box::new(LoggingPlugin),
    ];
    
    for plugin in &mut plugins {
        plugin.initialize().await?;
    }
    
    for plugin in &plugins {
        let result = plugin.execute("Hello, World!").await?;
        println!("Result: {}", result);
    }
    
    for plugin in &mut plugins {
        plugin.shutdown().await?;
    }
    
    Ok(())
}

Default Implementations

use async_trait::async_trait;
 
#[async_trait]
pub trait AsyncProcessor {
    async fn process(&self, data: &str) -> String;
    
    // Default implementation calling process
    async fn process_batch(&self, items: &[&str]) -> Vec<String> {
        let mut results = Vec::new();
        for item in items {
            results.push(self.process(item).await);
        }
        results
    }
}
 
struct UppercaseProcessor;
 
#[async_trait]
impl AsyncProcessor for UppercaseProcessor {
    async fn process(&self, data: &str) -> String {
        tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
        data.to_uppercase()
    }
}
 
// Usage
async fn demo() {
    let processor = UppercaseProcessor;
    let results = processor.process_batch(&["hello", "world"]).await;
    println!("{:?}", results); // ["HELLO", "WORLD"]
}

Summary

  • Apply #[async_trait] to both the trait definition and its implementations
  • Async trait methods return Pin<Box<dyn Future>> under the hood, enabling dynamic dispatch
  • Use &dyn Trait or Box<dyn Trait> to store and pass around trait objects with async methods
  • The trait and its implementations require Send bound for use across threads (add : Send + Sync to trait bounds)
  • Generic traits work fine with async-trait: trait Repository<T> { ... }
  • Default method implementations can call other async methods on &self
  • For new projects targeting Rust 1.75+, consider native async fn in traits first, but async-trait remains essential for object-safe traits with complex bounds
  • Common use cases: database abstractions, plugin systems, middleware, and async service traits