| name | async-trait |
| description | Expert knowledge for the Rust async-trait crate — the |
| hash | async-trait-skill-v1 |
async-trait
A procedural macro by David Tolnay that enables async functions in traits to work with dynamic dispatch (dyn Trait). Essential when you need trait objects with async methods.
Version: 0.1.89 (latest as of 2025)
Use for: Dynamic dispatch with async traits, plugin systems, dependency injection with async interfaces.
When to Use async-trait vs Native Async Traits
| Scenario | Solution |
|---|
| Static dispatch only (generics) | Native async fn in traits (Rust 1.75+) |
Need dyn Trait / trait objects | #[async_trait] required |
| Pre-Rust 1.75 compatibility | #[async_trait] required |
| Performance-critical tight loops | Consider avoiding trait objects entirely |
Key insight: Native async traits (Rust 1.75+) do NOT support dyn Trait. If you need trait objects with async methods, async-trait is still required.
Core Usage
Apply #[async_trait] to both trait definition AND all implementations:
use async_trait::async_trait;
#[async_trait]
pub trait ModelScanner: Send + Sync {
async fn scan(&self) -> Result<Vec<Model>, ScanError>;
async fn is_available(&self) -> bool;
fn name(&self) -> &'static str;
}
#[async_trait]
impl ModelScanner for OllamaScanner {
async fn scan(&self) -> Result<Vec<Model>, ScanError> {
}
async fn is_available(&self) -> bool {
self.client.health_check().await.is_ok()
}
fn name(&self) -> &'static str {
"ollama"
}
}
Macro Transformation
The macro transforms async methods into boxed futures:
async fn scan(&self) -> Vec<Model>;
fn scan<'async_trait>(&'async_trait self)
-> Pin<Box<dyn Future<Output = Vec<Model>> + Send + 'async_trait>>
where
Self: Sync + 'async_trait
{
Box::pin(async move { })
}
Topics
Bounds and Constraints
- Send Bounds - Default Send requirement, ?Send variant, thread safety
Performance
Patterns
Send + Sync Bounds
Default behavior: Futures are Send (can move between threads).
#[async_trait]
trait MyTrait {
async fn method(&self);
}
For single-threaded contexts (e.g., !Send types, Rc, RefCell):
#[async_trait(?Send)]
trait LocalTrait {
async fn method(&self);
}
#[async_trait(?Send)]
impl LocalTrait for MyType {
async fn method(&self) { }
}
Trait Object Pattern
use async_trait::async_trait;
#[async_trait]
pub trait Scanner: Send + Sync {
async fn scan(&self) -> Vec<Item>;
}
pub struct Registry {
scanners: Vec<Box<dyn Scanner>>,
}
impl Registry {
pub fn add(&mut self, scanner: impl Scanner + 'static) {
self.scanners.push(Box::new(scanner));
}
pub async fn scan_all(&self) -> Vec<Item> {
let futures: Vec<_> = self.scanners.iter().map(|s| s.scan()).collect();
futures::future::join_all(futures).await.into_iter().flatten().collect()
}
}
Performance Implications
Overhead per call: ~20 nanoseconds (heap allocation for boxed future)
When it matters:
- Millions of calls per second in tight loops
- Embedded/microcontroller environments
- Latency-critical hot paths
When it doesn't matter (most cases):
- Web servers, API handlers
- Database operations
- File I/O
- Network requests
Benchmark perspective: 100K calls = ~2ms overhead. Usually negligible compared to actual I/O.
Common Mistakes
Forgetting macro on impl
#[async_trait]
trait MyTrait { async fn method(&self); }
impl MyTrait for MyType {
async fn method(&self) { }
}
#[async_trait]
impl MyTrait for MyType {
async fn method(&self) { }
}
Mismatched Send bounds
#[async_trait]
trait MyTrait { ... }
#[async_trait(?Send)]
impl MyTrait for MyType { ... }
Missing trait bounds for trait objects
#[async_trait]
trait BadTrait {
async fn method(&self);
}
#[async_trait]
trait GoodTrait: Send + Sync {
async fn method(&self);
}
let scanner: Box<dyn GoodTrait> = Box::new(MyImpl);
Lifetime Elision
Async-trait supports lifetime elision in & and &mut references only:
#[async_trait]
trait Valid {
async fn process(&self, data: &str);
}
#[async_trait]
trait NeedsExplicit {
async fn process(&self, data: Cow<'_, str>);
}
Alternative: trait_variant (Native + Send)
For native async traits that need Send bounds without full boxing:
use trait_variant::make;
#[trait_variant::make(SendScanner: Send)]
trait LocalScanner {
async fn scan(&self) -> Vec<Model>;
}
Limitation: Still no dyn Trait support - use async-trait for that.
Resources