What is the purpose of async_trait::async_trait on both trait definitions and implementation blocks?
The async_trait attribute is required on both the trait definition and the implementation block because it performs different but complementary transformations: on the trait definition, it converts async method signatures into trait-compatible Pin<Box<dyn Future>> return types, and on the implementation block, it wraps the async method bodies in Box::pin to satisfy the trait's transformed signature. Without the attribute on both sides, either the trait definition would be invalid Rust (async fn in traits isn't natively supported in older Rust versions) or the implementation wouldn't match the trait's expected return type.
The Problem: Async Functions in Traits
// This doesn't work in Rust versions before 1.75:
// trait AsyncService {
// async fn fetch(&self, url: &str) -> String;
// }
// Error: async fn in trait is not supported
// Traits cannot declare async methods directly before async_fn_in_trait stabilizationBefore Rust 1.75 stabilized async fn in traits, declaring async methods in traits was not allowed because the compiler couldn't properly represent the returned future in the trait object vtable.
What async_trait Does to Trait Definitions
use async_trait::async_trait;
#[async_trait]
trait AsyncService {
async fn fetch(&self, url: &str) -> String;
}
// After macro expansion, the trait becomes:
trait AsyncService {
fn fetch<'life0, 'async_trait>(
&'life0 self,
url: &'life0 str,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = String> + Send + 'async_trait>>
where
'life0: 'async_trait,
Self: 'async_trait;
}The async_trait attribute transforms async method signatures into synchronous functions that return boxed, pinned futures with complex lifetime bounds.
What async_trait Does to Implementations
use async_trait::async_trait;
struct MyService;
#[async_trait]
impl AsyncService for MyService {
async fn fetch(&self, url: &str) -> String {
// async body
String::from("data")
}
}
// After macro expansion:
impl AsyncService for MyService {
fn fetch<'life0, 'async_trait>(
&'life0 self,
url: &'life0 str,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = String> + Send + 'async_trait>>
where
'life0: 'async_trait,
Self: 'async_trait,
{
Box::pin(async move {
// original async body
String::from("data")
})
}
}The implementation's async method body gets wrapped in Box::pin, creating a future that satisfies the trait's return type.
Why Both Sides Need the Attribute
use async_trait::async_trait;
// WITHOUT attribute on trait definition - INVALID:
// trait AsyncService {
// async fn fetch(&self) -> String; // Error: async fn in trait not supported
// }
// WITHOUT attribute on implementation - TYPE MISMATCH:
// impl AsyncService for MyService {
// async fn fetch(&self) -> String { // Wrong return type!
// String::from("data")
// }
// }
// Error: method `fetch` has an incompatible type for trait
// Expected: Pin<Box<dyn Future<Output = String>>>
// Found: impl Future<Output = String>
// The async_trait attribute on BOTH sides makes them compatible:
#[async_trait]
trait AsyncService {
async fn fetch(&self) -> String;
}
#[async_trait]
impl AsyncService for MyService {
async fn fetch(&self) -> String {
String::from("data")
}
}Both sides need the attribute because the trait definition's signature changes (requiring Pin<Box<dyn Future>>), and the implementation must match that transformed signature.
The Transformation in Detail
use async_trait::async_trait;
#[async_trait]
trait Example {
async fn method(&self);
}
// The transformation involves:
// 1. Converting `async fn` to `fn`
// 2. Wrapping return type in Pin<Box<dyn Future<Output = ...>>>
// 3. Adding complex lifetime bounds for all references
// 4. Adding 'async_trait bound on Self
// Implementation transformation:
// 1. Converting `async fn` to regular `fn`
// 2. Wrapping the async body in Box::pin(async move { ... })
// 3. The resulting type matches the trait's expected Pin<Box<...>>The attribute performs identical signature transformations on both sides to ensure type compatibility.
The 'async_trait Lifetime Explained
use async_trait::async_trait;
#[async_trait]
trait Database {
async fn query(&self, sql: &str) -> Vec<String>;
}
// Transformed signature includes 'async_trait lifetime:
// fn query<'life0, 'async_trait>(
// &'life0 self,
// sql: &'life0 str,
// ) -> Pin<Box<dyn Future<Output = Vec<String>> + Send + 'async_trait>>
// where
// 'life0: 'async_trait,
// Self: 'async_trait;
// 'async_trait bounds ensure:
// 1. The returned future doesn't outlive the method parameters
// 2. Self lives long enough for the future's execution
// 3. All borrowed values have appropriate lifetimesThe generated 'async_trait lifetime bound captures all borrowed parameters to ensure the future doesn't outlive its inputs.
Send Bounds and Thread Safety
use async_trait::async_trait;
// Default: Requires Send on futures (for thread-safe contexts)
#[async_trait]
trait ThreadSafeService {
async fn run(&self) -> i32;
}
// Generated futures have + Send bound:
// Pin<Box<dyn Future<Output = i32> + Send + 'async_trait>>
// For non-Send futures (single-threaded contexts):
#[async_trait(?Send)]
trait SingleThreadedService {
async fn run(&self) -> i32;
}
// Generated futures omit Send bound:
// Pin<Box<dyn Future<Output = i32> + 'async_trait>>The attribute adds Send bounds by default for thread safety; use ?Send to opt out for single-threaded runtimes.
Multiple Methods in a Trait
use async_trait::async_trait;
#[async_trait]
trait Service {
async fn connect(&self, addr: &str) -> bool;
async fn disconnect(&self) -> Result<(), Error>;
fn sync_method(&self) -> i32; // Regular sync method, no transformation
}
#[async_trait]
impl Service for MyService {
async fn connect(&self, addr: &str) -> bool {
// async body gets boxed
true
}
async fn disconnect(&self) -> Result<(), Error> {
// async body gets boxed
Ok(())
}
fn sync_method(&self) -> i32 {
// Regular method, unchanged
42
}
}Only async methods are transformed; synchronous methods pass through unchanged.
Without async_trait: Manual Implementation
use std::future::Future;
use std::pin::Pin;
// Manual implementation without async_trait attribute:
trait ManualService {
fn fetch<'life0, 'async_trait>(
&'life0 self,
url: &'life0 str,
) -> Pin<Box<dyn Future<Output = String> + Send + 'async_trait>>
where
'life0: 'async_trait,
Self: 'async_trait;
}
impl ManualService for MyService {
fn fetch<'life0, 'async_trait>(
&'life0 self,
url: &'life0 str,
) -> Pin<Box<dyn Future<Output = String> + Send + 'async_trait>>
where
'life0: 'async_trait,
Self: 'async_trait,
{
Box::pin(async move {
String::from("data")
})
}
}
// The async_trait attribute eliminates this boilerplateWriting the transformed signatures manually is verbose and error-prone; the attribute automates this.
Modern Rust: Native Async in Traits
// Rust 1.75+ supports native async fn in traits:
// This now works without async_trait:
trait NativeAsyncService {
async fn fetch(&self, url: &str) -> String;
}
impl NativeAsyncService for MyService {
async fn fetch(&self, url: &str) -> String {
String::from("data")
}
}
// However, async_trait is still useful for:
// 1. Compatibility with older Rust versions
// 2. Trait objects (dyn Trait) - requires Box<dyn Future>
// 3. Complex lifetime scenarios
// 4. Send bounds customization (?Send)Native async in traits is now stable, but async_trait remains useful for trait objects and compatibility.
Trait Objects Require async_trait
use async_trait::async_trait;
#[async_trait]
trait DynService: Send + Sync {
async fn execute(&self) -> i32;
}
// For trait objects (dyn DynService), async_trait is necessary:
#[async_trait]
impl DynService for MyServiceImpl {
async fn execute(&self) -> i32 {
42
}
}
// Using trait object:
fn use_dyn(service: &dyn DynService) {
// This requires async_trait because:
// - dyn Future is not object-safe
// - Box<dyn Future> is object-safe
// - async_trait provides the boxing
}
// Native async fn in traits cannot be used with dyn:
// trait NotObjectSafe {
// async fn method(&self); // Returns impl Future, not object-safe
// }Trait objects (dyn Trait) still require async_trait because native async traits return impl Future which is not object-safe.
Generic Trait Methods
use async_trait::async_trait;
#[async_trait]
trait GenericService {
// Generic methods with async_trait:
async fn process<T: Send + 'static>(&self, item: T) -> String;
}
#[async_trait]
impl GenericService for MyService {
async fn process<T: Send + 'static>(&self, item: T) -> String {
format!("{:?}", std::any::type_name::<T>())
}
}
// The generic bounds are preserved through transformation
// Send bound on T ensures the future can be sent across threadsGeneric methods work with async_trait, but bounds must ensure thread safety for the returned future.
Default Implementations
use async_trait::async_trait;
#[async_trait]
trait ServiceWithDefault {
async fn fetch(&self) -> String {
// Default async implementation
String::from("default")
}
async fn process(&self, data: String) -> String {
// Can call other async methods
let fetched = self.fetch().await;
format!("{}: {}", fetched, data)
}
}
#[async_trait]
impl ServiceWithDefault for MyService {
// Override only fetch
async fn fetch(&self) -> String {
String::from("custom")
}
// process uses default implementation
}Default implementations in traits work with async_trait, allowing override in specific implementations.
Where Clauses and Bounds
use async_trait::async_trait;
#[async_trait]
trait BoundedService<T: Send + 'static> {
async fn process(&self, item: T) -> Result<T, Error>;
}
#[async_trait]
impl<T: Send + 'static> BoundedService<T> for MyService {
async fn process(&self, item: T) -> Result<T, Error> {
Ok(item)
}
}
// The async_trait preserves all bounds in the transformed signatures
// Generic bounds flow through to the boxed futureGeneric bounds and where clauses are preserved in the transformation.
Comparison: With vs Without async_trait
use async_trait::async_trait;
// WITH async_trait - clean, readable:
#[async_trait]
trait CleanService {
async fn run(&self) -> i32;
}
#[async_trait]
impl CleanService for MyService {
async fn run(&self) -> i32 {
42
}
}
// WITHOUT async_trait - manual boxing:
trait ManualService {
fn run<'life0, 'async_trait>(
&'life0 self,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = i32> + Send + 'async_trait>
>
where
'life0: 'async_trait,
Self: 'async_trait;
}
impl ManualService for MyService {
fn run<'life0, 'async_trait>(
&'life0 self,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = i32> + Send + 'async_trait>
>
where
'life0: 'async_trait,
Self: 'async_trait,
{
std::boxed::Box::pin(std::future::ready(42))
}
}The attribute reduces boilerplate significantly while ensuring signature compatibility.
Summary Table
fn summary_table() {
// | Location | Attribute Purpose |
// |----------|-------------------|
// | Trait definition | Transforms `async fn` to `fn -> Pin<Box<dyn Future>>` |
// | Implementation block | Wraps async body in `Box::pin(async { ... })` |
// | Without on Trait | Without on Impl |
// |-------------------|-----------------|
// | Compile error: async fn in trait not supported | Type mismatch: impl Future vs Pin<Box<dyn Future>> |
// | Use Case | async_trait Needed? |
// |----------|---------------------|
// | Trait objects (dyn Trait) | Yes |
// | Older Rust versions (<1.75) | Yes |
// | Rust 1.75+ without dyn | Optional (native works) |
// | ?Send futures (single-threaded) | Yes |
}Synthesis
Quick reference:
use async_trait::async_trait;
#[async_trait]
trait Service {
async fn execute(&self) -> String;
}
#[async_trait]
impl Service for MyService {
async fn execute(&self) -> String {
String::from("result")
}
}
// Both attributes are required:
// - Trait: Converts async fn to fn -> Pin<Box<dyn Future>>
// - Impl: Wraps body in Box::pin to match trait signatureKey insight: The async_trait attribute must appear on both the trait definition and implementation block because it performs complementary transformations: on the trait side, it converts async fn into a regular function that returns Pin<Box<dyn Future>> (since traits cannot natively express the existential impl Future return type before Rust 1.75), and on the implementation side, it wraps the async method body in Box::pin(async move { ... }) to produce the boxed future that matches the trait's expected return type. Without the attribute on both sides, either the trait definition would be invalid Rust syntax, or the implementation would have a type mismatch—the trait expects Pin<Box<dyn Future>> but a bare async fn returns impl Future, which are incompatible types. The attribute also handles complex lifetime bounds ('async_trait) that capture all borrowed parameters, ensuring the returned future doesn't outlive its inputs. While Rust 1.75 stabilized native async fn in traits for concrete types, async_trait remains necessary for trait objects (dyn Trait) and code that must support older Rust versions.
