How do I define async traits in Rust?

Walkthrough

Rust doesn't natively support async functions in traits (though this is changing with recent Rust versions). The async-trait crate provides a procedural macro that makes this possible by boxing futures. It's essential for building async abstractions and polymorphic async code.

Why async-trait matters:

  1. Without it, you can't declare async methods in traits—the compiler will reject them
  2. The macro transforms async methods into regular methods returning Pin<Box<dyn Future>>
  3. It enables clean, idiomatic code that reads naturally despite the underlying complexity
  4. Works with trait bounds, generics, and complex return types

Note: Rust 1.75+ supports native async trait methods (RPITIT), but async-trait remains useful for compatibility with older Rust versions and for certain advanced patterns.

Code Example

# Cargo.toml
[dependencies]
async-trait = "0.1"
tokio = { version = "1", features = ["full"] }
use async_trait::async_trait;
use std::error::Error;
 
// Define an async trait using the macro
#[async_trait]
pub trait Database {
    async fn connect(&mut self, connection_string: &str) -> Result<(), Box<dyn Error>>;
    async fn query(&self, sql: &str) -> Result<Vec<String>, Box<dyn Error>>;
    async fn disconnect(&mut self) -> Result<(), Box<dyn Error>>;
}
 
// Implement the trait for a PostgreSQL-like database
struct PostgresDatabase {
    connected: bool,
}
 
#[async_trait]
impl Database for PostgresDatabase {
    async fn connect(&mut self, connection_string: &str) -> Result<(), Box<dyn Error>> {
        println!("Connecting to PostgreSQL: {}", connection_string);
        // Simulate async connection
        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
        self.connected = true;
        Ok(())
    }
 
    async fn query(&self, sql: &str) -> Result<Vec<String>, Box<dyn Error>> {
        if !self.connected {
            return Err("Not connected".into());
        }
        println!("Executing query: {}", sql);
        Ok(vec![
            "row1".to_string(),
            "row2".to_string(),
        ])
    }
 
    async fn disconnect(&mut self) -> Result<(), Box<dyn Error>> {
        println!("Disconnecting from PostgreSQL");
        self.connected = false;
        Ok(())
    }
}
 
// Implement the trait for a mock database (useful for testing)
struct MockDatabase {
    records: Vec<String>,
}
 
#[async_trait]
impl Database for MockDatabase {
    async fn connect(&mut self, _connection_string: &str) -> Result<(), Box<dyn Error>> {
        Ok(())
    }
 
    async fn query(&self, _sql: &str) -> Result<Vec<String>, Box<dyn Error>> {
        Ok(self.records.clone())
    }
 
    async fn disconnect(&mut self) -> Result<(), Box<dyn Error>> {
        Ok(())
    }
}
 
// Function that works with any type implementing Database
async fn fetch_users(db: &dyn Database) -> Result<Vec<String>, Box<dyn Error>> {
    let users = db.query("SELECT * FROM users").await?;
    Ok(users)
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    // Use PostgresDatabase
    let mut postgres = PostgresDatabase { connected: false };
    postgres.connect("postgresql://localhost/mydb").await?;
    
    let users = fetch_users(&postgres).await?;
    println!("Users: {:?}", users);
    
    postgres.disconnect().await?;
 
    // Use MockDatabase for testing
    let mock = MockDatabase {
        records: vec!["test_user".to_string()],
    };
    let test_users = fetch_users(&mock).await?;
    println!("Test users: {:?}", test_users);
 
    Ok(())
}

Async Trait with Generics

use async_trait::async_trait;
 
#[async_trait]
pub trait Cache<K, V> {
    async fn get(&self, key: &K) -> Option<V>;
    async fn set(&mut self, key: K, value: V) -> bool;
    async fn remove(&mut self, key: &K) -> Option<V>;
}
 
struct InMemoryCache<K, V> {
    data: std::collections::HashMap<K, V>,
}
 
#[async_trait]
impl<K, V> Cache<K, V> for InMemoryCache<K, V>
where
    K: std::hash::Hash + Eq + Clone + Send + Sync + 'static,
    V: Clone + Send + Sync + 'static,
{
    async fn get(&self, key: &K) -> Option<V> {
        self.data.get(key).cloned()
    }
 
    async fn set(&mut self, key: K, value: V) -> bool {
        self.data.insert(key, value).is_some()
    }
 
    async fn remove(&mut self, key: &K) -> Option<V> {
        self.data.remove(key)
    }
}

Summary

  • Add #[async_trait] attribute to both trait definition and impl blocks
  • Async methods become regular methods returning Pin<Box<dyn Future>> behind the scenes
  • Useful for dependency injection, polymorphism, and testing with mock implementations
  • Requires Send bounds for types used across await points (the macro handles this by default)
  • Native async traits (RPITIT) are available in Rust 1.75+ but async-trait remains valuable for older versions and certain use cases
  • The slight runtime overhead from boxing is usually negligible compared to actual async I/O operations