Loading pageâŚ
Rust walkthroughs
Loading pageâŚ
async_trait work around the limitation that traits with async methods cannot be object-safe?Async methods in traits present a fundamental challenge in Rust's type system: async functions return opaque impl Future types, which cannot be named, stored in vtables, or used as trait object methods. The async_trait macro solves this through a transformation that trades some performance for the ability to use dyn Trait with async methods.
When you write an async function, the compiler generates a state machine type that implements Future:
async fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
// The compiler transforms this into something like:
fn greet(name: String) -> impl Future<Output = String> {
GreetFuture { state: Initial, name }
}The actual type GreetFuture is compiler-generated and has no stable name. This works fine until you try to put it in a trait:
// This won't compile
trait Greeter {
async fn greet(&self, name: String) -> String;
}
fn use_greeter(greeter: &dyn Greeter) { // ERROR!
// ...
}The error occurs because vtables require known type sizes for return values, but impl Future is an opaque type with an unknown size at compile time.
A trait object (dyn Trait) uses a vtableâa table of function pointers. Each entry must have a concrete signature:
// Vtable layout (conceptual)
struct Vtable {
greet: fn(*const (), String) -> ???,
// return type?
}For non-async methods, the return type is known:
trait Greeter {
fn greet(&self, name: String) -> String; // String is Sized
}But async methods return impl Future<Output = String>, which is a different concrete type for each implementation:
struct EnglishGreeter;
impl Greeter for EnglishGreeter {
async fn greet(&self, name: String) -> String {
// Returns EnglishGreetFuture
format!("Hello, {}!", name)
}
}
struct FrenchGreeter;
impl Greeter for FrenchGreeter {
async fn greet(&self, name: String) -> String {
// Returns FrenchGreetFuture (different type!)
format!("Bonjour, {}!", name)
}
}These futures have different sizes and layouts, making them impossible to store in a uniform vtable slot.
The async_trait macro transforms async methods into methods that return Pin<Box<dyn Future>>:
use async_trait::async_trait;
#[async_trait]
trait Greeter {
async fn greet(&self, name: String) -> String;
}
// The macro expands to:
trait Greeter {
fn greet<'life0, 'async_trait>(
&'life0 self,
name: String,
) -> Pin<Box<dyn Future<Output = String> + Send + 'async_trait>>
where
'life0: 'async_trait,
Self: 'async_trait;
}The boxed dyn Future has a known size (it's a pointer), so it fits in a vtable. Each implementation boxes its specific future type:
#[async_trait]
impl Greeter for EnglishGreeter {
async fn greet(&self, name: String) -> String {
format!("Hello, {}!", name)
}
}
// Expands to:
impl Greeter for EnglishGreeter {
fn greet<'life0, 'async_trait>(
&'life0 self,
name: String,
) -> Pin<Box<dyn Future<Output = String> + Send + 'async_trait>>
where
'life0: 'async_trait,
Self: 'async_trait,
{
Box::pin(async move {
format!("Hello, {}!", name)
})
}
}Now the trait is object-safe:
use async_trait::async_trait;
#[async_trait]
trait Greeter {
async fn greet(&self, name: String) -> String;
}
struct EnglishGreeter;
struct FrenchGreeter;
#[async_trait]
impl Greeter for EnglishGreeter {
async fn greet(&self, name: String) -> String {
format!("Hello, {}!", name)
}
}
#[async_trait]
impl Greeter for FrenchGreeter {
async fn greet(&self, name: String) -> String {
format!("Bonjour, {}!", name)
}
}
async fn use_greeter(greeter: &dyn Greeter) {
let message = greeter.greet("World".to_string()).await;
println!("{}", message);
}
#[tokio::main]
async fn main() {
let greeters: Vec<Box<dyn Greeter>> = vec![
Box::new(EnglishGreeter),
Box::new(FrenchGreeter),
];
for greeter in &greeters {
use_greeter(greeter.as_ref()).await;
}
}The expanded signatures include complex lifetime bounds that ensure safety:
fn greet<'life0, 'async_trait>(
&'life0 self,
name: String,
) -> Pin<Box<dyn Future<Output = String> + Send + 'async_trait>>
where
'life0: 'async_trait,
Self: 'async_trait;The 'async_trait lifetime is syntheticâit captures the relationship between borrowed inputs and the returned future. When self is borrowed, the future must not outlive it:
#[async_trait]
trait Database {
async fn query(&self, sql: &str) -> Vec<Row>;
}
// The future captures &'self str, so it must not outlive the referenceThe bounds 'life0: 'async_trait ensure the returned future doesn't outlive any borrowed references.
Boxing has measurable overhead:
use async_trait::async_trait;
// Without async_trait (not object-safe):
trait FastGreeter {
fn greet(&self, name: String) -> impl Future<Output = String> + Send;
}
// With async_trait (object-safe but boxed):
#[async_trait]
trait BoxedGreeter {
async fn greet(&self, name: String) -> String;
}The costs include:
poll methodFor hot paths, this overhead can be significant:
use async_trait::async_trait;
use std::time::Instant;
#[async_trait]
trait Processor {
async fn process(&self, data: &[u8]) -> Vec<u8>;
}
struct EchoProcessor;
#[async_trait]
impl Processor for EchoProcessor {
async fn process(&self, data: &[u8]) -> Vec<u8> {
data.to_vec() // Simple echo
}
}
async fn benchmark(processor: &dyn Processor, iterations: usize) {
let start = Instant::now();
let data = vec![0u8; 1024];
for _ in 0..iterations {
let _ = processor.process(&data).await;
}
println!("Time: {:?}", start.elapsed());
}Each call allocates a boxed future, adding latency compared to a static dispatch version.
The #[async_trait] macro adds Send bound by default. For non-Send futures, use #[async_trait(?Send)]:
#[async_trait]
trait SendGreeter {
// Generated future is Send
async fn greet(&self) -> String;
}
#[async_trait(?Send)]
trait LocalGreeter {
// Generated future may not be Send
async fn greet(&self) -> String;
}
// Example requiring ?Send
struct ThreadLocalGreeter {
data: std::cell::RefCell<String>, // Not Send
}
#[async_trait(?Send)]
impl LocalGreeter for ThreadLocalGreeter {
async fn greet(&self) -> String {
self.data.borrow().clone()
}
}The ?Send variant is useful for single-threaded runtimes or when using !Send types like Rc or RefCell inside async blocks.
You can implement the same transformation manually:
use std::future::Future;
use std::pin::Pin;
trait Greeter {
fn greet(
&self,
name: String,
) -> Pin<Box<dyn Future<Output = String> + Send + '_>>;
}
struct EnglishGreeter;
impl Greeter for EnglishGreeter {
fn greet(
&self,
name: String,
) -> Pin<Box<dyn Future<Output = String> + Send + '_>> {
Box::pin(async move {
format!("Hello, {}!", name)
})
}
}This gives you full control over the transformation but requires verbose syntax. The macro automates this boilerplate.
Rust is actively working on native support for async methods in traits without boxing. The approach uses return-position impl Trait in trait (RPITIT):
// Future Rust (stabilized in Rust 1.75+, but not object-safe)
trait Greeter {
async fn greet(&self, name: String) -> String;
}However, this still isn't object-safe. The trait can be used with generics, but not with dyn Greeter:
// Works with generics
async fn use_generic<G: Greeter>(greeter: &G) {
greeter.greet("World".to_string()).await;
}
// Still doesn't work with dyn
async fn use_dyn(greeter: &dyn Greeter) { // ERROR
greeter.greet("World".to_string()).await;
}The async_trait macro remains necessary when you need dynamic dispatch with async methods.
Use async_trait when:
use async_trait::async_trait;
// 1. You need trait objects
fn process_handlers(handlers: Vec<Box<dyn Handler>>) { /* ... */ }
// 2. You're building a plugin system
#[async_trait]
trait Plugin {
async fn initialize(&mut self) -> Result<(), Error>;
async fn process(&self, input: &[u8]) -> Result<Vec<u8>, Error>;
}
// 3. You're implementing middleware chains
#[async_trait]
trait Middleware {
async fn handle(&self, request: Request) -> Result<Response, Error>;
}Avoid async_trait when:
// 1. You can use generics instead
trait Handler {
async fn handle(&self, request: Request) -> Response;
}
async fn process<H: Handler>(handler: &H, request: Request) -> Response {
handler.handle(request).await
}
// 2. The async method is performance-critical
// Use generics to enable inlining and avoid boxing
// 3. You only need one implementation
// Just use a struct directlyasync_trait works well with other trait patterns:
use async_trait::async_trait;
use std::sync::Arc;
#[async_trait]
trait Repository: Send + Sync {
async fn find_by_id(&self, id: u64) -> Option<Record>;
async fn save(&self, record: &Record) -> Result<(), Error>;
}
struct PostgresRepository {
pool: Arc<Pool>,
}
#[async_trait]
impl Repository for PostgresRepository {
async fn find_by_id(&self, id: u64) -> Option<Record> {
let row = self.pool.query_opt(
"SELECT * FROM records WHERE id = $1",
&[&id],
).await.ok().flatten()?;
Some(Record::from_row(row))
}
async fn save(&self, record: &Record) -> Result<(), Error> {
self.pool.execute(
"INSERT INTO records (id, data) VALUES ($1, $2)",
&[&record.id, &record.data],
).await?;
Ok(())
}
}
// Now you can use dyn Repository
struct Service {
repo: Arc<dyn Repository>,
}
impl Service {
async fn get_record(&self, id: u64) -> Option<Record> {
self.repo.find_by_id(id).await
}
}The async_trait macro enables object-safe traits with async methods by transforming each async method into a synchronous method that returns a boxed, pinned future. This transformation makes the return type concrete (Pin<Box<dyn Future>>) with a known size, allowing it to be stored in a vtable.
The cost is heap allocation and dynamic dispatch on every call, plus the loss of inlining opportunities. For most applicationsâespecially those building plugin systems, middleware chains, or any architecture requiring runtime polymorphismâthis trade-off is acceptable.
Native async traits (available since Rust 1.75) allow async methods in traits without macros, but they don't enable dynamic dispatch. The async_trait macro remains essential when you specifically need dyn Trait with async methods. Understanding this distinction helps you choose the right tool: generics for static dispatch and zero-cost abstractions, async_trait for dynamic dispatch with acceptable overhead.