What is the difference between async_trait::async_trait on trait definitions vs implementations?

The async_trait attribute macro works differently when applied to trait definitions versus trait implementations. On trait definitions, it transforms async methods into methods returning Pin<Box<dyn Future>>, enabling async methods in traits despite Rust's lack of native support for async trait methods. On trait implementations, it generates the corresponding boxing code to match the trait's transformed signature. Understanding this distinction reveals how the macro bridges the gap between Rust's current type system and the desired async trait ergonomics—and why both positions need the attribute for the transformation to work correctly.

Async Trait Definition

use async_trait::async_trait;
 
#[async_trait]
pub trait Database {
    // The macro transforms this async method
    async fn connect(&self, url: &str) -> Result<(), Error>;
    async fn query(&self, sql: &str) -> Result<Vec<Row>, Error>;
}
 
// What the macro actually generates:
pub trait Database {
    fn connect<'life0, 'life1, 'async_trait>(
        &'life0 self,
        url: &'life1 str,
    ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
    where
        'life0: 'async_trait,
        'life1: 'async_trait,
        Self: 'async_trait;
    
    fn query<'life0, 'life1, 'async_trait>(
        &'life0 self,
        sql: &'life1 str,
    ) -> Pin<Box<dyn Future<Output = Result<Vec<Row>, Error>> + Send + 'async_trait>>
    where
        'life0: 'async_trait,
        'life1: 'async_trait,
        Self: 'async_trait;
}

The trait definition uses #[async_trait] to transform async methods into boxed futures.

Async Trait Implementation

use async_trait::async_trait;
 
#[async_trait]
impl Database for PostgresClient {
    // The macro boxes the async body
    async fn connect(&self, url: &str) -> Result<(), Error> {
        // Async code works normally inside
        let connection = TcpStream::connect(url).await?;
        self.set_connection(connection).await;
        Ok(())
    }
    
    async fn query(&self, sql: &str) -> Result<Vec<Row>, Error> {
        self.execute(sql).await
    }
}
 
// What the macro generates:
impl Database for PostgresClient {
    fn connect<'life0, 'life1, 'async_trait>(
        &'life0 self,
        url: &'life1 str,
    ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
    where
        'life0: 'async_trait,
        'life1: 'async_trait,
        Self: 'async_trait,
    {
        Box::pin(async move {
            let connection = TcpStream::connect(url).await?;
            self.set_connection(connection).await;
            Ok(())
        })
    }
    
    // Similar for query...
}

The implementation uses #[async_trait] to wrap the async body in Box::pin.

Why Both Positions Need the Attribute

use async_trait::async_trait;
 
// WITHOUT #[async_trait] on trait definition:
// This would fail to compile:
trait BadAsyncTrait {
    async fn method(&self); // Error: async fn in trait not supported
}
 
// WITH #[async_trait] on trait definition but NOT on impl:
// The trait signature is transformed, but impl doesn't match
#[async_trait]
trait GoodTrait {
    async fn method(&self);
}
 
// This would fail - missing #[async_trait]
impl GoodTrait for SomeType {
    async fn method(&self) {
        // Error: method signature doesn't match transformed trait
    }
}
 
// Correct: both positions have #[async_trait]
#[async_trait]
impl GoodTrait for SomeType {
    async fn method(&self) {
        // Works correctly
    }
}

Both positions need the attribute for signatures to match after transformation.

Send Bound Implications

use async_trait::async_trait;
 
// With Send bound (default)
#[async_trait]
pub trait AsyncService: Send + Sync {
    async fn process(&self, data: String) -> Result<String, Error>;
}
 
// Generated signature includes Send:
// fn process(...) -> Pin<Box<dyn Future<Output = Result<String, Error>> + Send + 'async_trait>>
 
// Without Send bound
#[async_trait(?Send)]
pub trait LocalService {
    async fn process(&self, data: String) -> Result<String, Error>;
}
 
// Generated signature without Send:
// fn process(...) -> Pin<Box<dyn Future<Output = Result<String, Error>> + 'async_trait>>

#[async_trait] adds Send bound; #[async_trait(?Send)] omits it for single-threaded contexts.

Definition-Side Behavior

use async_trait::async_trait;
 
#[async_trait]
pub trait Repository {
    // Methods with different signatures
    async fn get(&self, id: u64) -> Option<Item>;
    async fn save(&mut self, item: Item) -> Result<(), Error>;
    
    // Default implementation
    async fn get_or_default(&self, id: u64) -> Item {
        self.get(id).await.unwrap_or_default()
    }
}
 
// The trait definition handles:
// 1. Transforming method signatures to return boxed futures
// 2. Adding lifetime parameters for captured references
// 3. Adding Send bounds (or not, with ?Send)
// 4. Generating default implementations as boxed futures

The definition side transforms all method signatures uniformly.

Implementation-Side Behavior

use async_trait::async_trait;
 
struct CacheRepository {
    cache: HashMap<u64, Item>,
}
 
#[async_trait]
impl Repository for CacheRepository {
    async fn get(&self, id: u64) -> Option<Item> {
        // Can use .await normally inside
        tokio::time::sleep(Duration::from_millis(10)).await;
        self.cache.get(&id).cloned()
    }
    
    async fn save(&mut self, item: Item) -> Result<(), Error> {
        self.cache.insert(item.id, item);
        Ok(())
    }
    
    // Can override default implementation
    async fn get_or_default(&self, id: u64) -> Item {
        // Custom implementation
        self.get(id).await.unwrap_or(Item::default())
    }
}
 
// The impl side handles:
// 1. Wrapping async bodies in Box::pin
// 2. Matching the transformed trait signatures
// 3. Generating the correct lifetime annotations

The implementation side wraps method bodies in pinned boxes.

Lifetime Handling

use async_trait::async_trait;
 
#[async_trait]
pub trait Processor {
    // References in parameters work normally
    async fn process(&self, input: &str) -> String;
    
    // But returning references is more complex
    async fn get_ref(&self) -> &str; // Works due to 'self lifetime
}
 
#[async_trait]
impl Processor for StringProcessor {
    async fn process(&self, input: &str) -> String {
        input.to_uppercase()
    }
    
    async fn get_ref(&self) -> &str {
        &self.buffer
    }
}
 
// Generated code adds necessary lifetime bounds:
// fn process<'life0, 'life1, 'async_trait>(
//     &'life0 self,
//     input: &'life1 str,
// ) -> Pin<Box<dyn Future<Output = String> + Send + 'async_trait>>
// where
//     'life0: 'async_trait,
//     'life1: 'async_trait,
//     Self: 'async_trait;

The macro handles complex lifetime relationships automatically.

Trait Objects and Dynamic Dispatch

use async_trait::async_trait;
 
#[async_trait]
pub trait Service {
    async fn execute(&self, request: Request) -> Response;
}
 
#[async_trait]
impl Service for ServiceA {
    async fn execute(&self, request: Request) -> Response {
        Response::from_a(request)
    }
}
 
#[async_trait]
impl Service for ServiceB {
    async fn execute(&self, request: Request) -> Response {
        Response::from_b(request)
    }
}
 
async fn use_dynamic_dispatch(service: &dyn Service) {
    // Works because of the boxed future transformation
    let response = service.execute(Request::default()).await;
}
 
async fn dispatch_example() {
    let a = ServiceA {};
    let b = ServiceB {};
    
    // Can use as trait objects
    let services: Vec<&dyn Service> = vec![&a, &b];
    
    for service in services {
        let response = service.execute(Request::new()).await;
    }
}

The boxing enables trait objects with async methods, which wouldn't work natively.

Generic Methods

use async_trait::async_trait;
 
#[async_trait]
pub trait AsyncFactory {
    // Generic method in trait
    async fn create<T: Serialize + Send>(&self, data: T) -> Result<String, Error>;
}
 
#[async_trait]
impl AsyncFactory for JsonFactory {
    async fn create<T: Serialize + Send>(&self, data: T) -> Result<String, Error> {
        let json = serde_json::to_string(&data)?;
        Ok(json)
    }
}
 
// Generated code includes generic bounds correctly

Generic methods work with proper bounds propagation.

Associated Types

use async_trait::async_trait;
 
#[async_trait]
pub trait AsyncIterator {
    type Item;
    
    async fn next(&mut self) -> Option<Self::Item>;
}
 
#[async_trait]
impl AsyncIterator for AsyncRange {
    type Item = u32;
    
    async fn next(&mut self) -> Option<u32> {
        self.current += 1;
        if self.current < self.max {
            Some(self.current)
        } else {
            None
        }
    }
}

Associated types work correctly with async methods.

Conditional Implementations

use async_trait::async_trait;
 
#[async_trait]
pub trait AsyncHandler {
    async fn handle(&self, request: Request) -> Response;
}
 
// Conditional implementation
#[async_trait]
impl AsyncHandler for Handler
where
    Handler: Clone + Send,
{
    async fn handle(&self, request: Request) -> Response {
        let cloned = self.clone();
        cloned.process(request).await
    }
}
 
// The macro preserves where clauses correctly

Where clauses are preserved in the transformation.

Multiple Traits

use async_trait::async_trait;
 
#[async_trait]
pub trait Read {
    async fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error>;
}
 
#[async_trait]
pub trait Write {
    async fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
}
 
#[async_trait]
impl Read for FileStream {
    async fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error> {
        self.inner.read(buf).await
    }
}
 
#[async_trait]
impl Write for FileStream {
    async fn write(&mut self, buf: &[u8]) -> Result<usize, Error> {
        self.inner.write(buf).await
    }
}
 
// Each trait implementation needs its own #[async_trait]

Each impl block for an async trait requires its own attribute.

Default Implementations

use async_trait::async_trait;
 
#[async_trait]
pub trait AsyncCache {
    async fn get(&self, key: &str) -> Option<String>;
    async fn set(&self, key: &str, value: String);
    
    // Default implementation using other async methods
    async fn get_or_compute(&self, key: &str, compute: impl Fn() -> String + Send) -> String {
        if let Some(value) = self.get(key).await {
            value
        } else {
            let value = compute();
            self.set(key, value.clone()).await;
            value
        }
    }
}
 
#[async_trait]
impl AsyncCache for MemoryCache {
    async fn get(&self, key: &str) -> Option<String> {
        self.data.get(key).cloned()
    }
    
    async fn set(&self, key: &str, value: String) {
        self.data.insert(key.to_string(), value);
    }
    
    // get_or_compute uses default implementation
}

Default implementations work because they're also transformed to boxed futures.

Send Bound Variations

use async_trait::async_trait;
 
// Standard: requires Send on future
#[async_trait]
pub trait ThreadPoolService {
    async fn process(&self) -> Result<(), Error>;
}
 
// Without Send: works in single-threaded contexts
#[async_trait(?Send)]
pub trait LocalService {
    async fn process(&self) -> Result<(), Error>;
}
 
// Implementations must match
#[async_trait]
impl ThreadPoolService for RemoteService {
    async fn process(&self) -> Result<(), Error> {
        // Must be Send-safe
        Ok(())
    }
}
 
#[async_trait(?Send)]
impl LocalService for LocalOnlyService {
    async fn process(&self) -> Result<(), Error> {
        // Can use Rc, RefCell, etc.
        let local = std::rc::Rc::new(42);
        Ok(())
    }
}
 
// Mismatched Send bounds fail to compile
#[async_trait(?Send)]
impl ThreadPoolService for BadImpl {
    // Error: trait requires Send, impl doesn't provide it
    async fn process(&self) -> Result<(), Error> {
        Ok(())
    }
}

The ?Send variant enables non-Send futures for single-threaded runtimes.

Comparison Table

Aspect Trait Definition Trait Implementation
Attribute position #[async_trait] trait Foo #[async_trait] impl Foo for Bar
Transforms Method signatures Method bodies
Generates Boxed future signatures Box::pin(async { ... }) wrapper
Required For any async trait For every impl of an async trait
Send bound Default includes Send Must match trait's Send requirement

Error Messages Without Proper Attribute

use async_trait::async_trait;
 
// Missing #[async_trait] on trait definition
trait AsyncWithoutAttr {
    async fn method(&self); // Error: async fn in trait not supported
}
 
// Missing #[async_trait] on implementation
#[async_trait]
trait GoodTrait {
    async fn method(&self);
}
 
impl GoodTrait for SomeType {
    async fn method(&self) {
        // Error: method signatures don't match
        // Trait expects: fn method() -> Pin<Box<dyn Future...>>
        // Impl provides: async fn method()
    }
}

Missing attributes produce confusing errors about signature mismatches.

Debugging Generated Code

use async_trait::async_trait;
 
// To see what the macro generates, use cargo expand:
// cargo expand --lib
 
// For this trait:
#[async_trait]
pub trait Example {
    async fn do_something(&self, input: String) -> Result<(), Error>;
}
 
// cargo expand shows the generated code:
pub trait Example {
    fn do_something<'life0, 'async_trait>(
        &'life0 self,
        input: String,
    ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
    where
        'life0: 'async_trait,
        Self: 'async_trait;
}

cargo expand reveals the actual generated signatures for debugging.

Synthesis

Definition-side responsibilities:

Responsibility Description
Signature transformation async fn → fn() -> Pin<Box<dyn Future>>
Lifetime generation Adds 'async_trait and capture lifetimes
Send bound Default adds Send to future bounds
Default bodies Transforms default implementations

Implementation-side responsibilities:

Responsibility Description
Body wrapping Wraps async { } in Box::pin
Signature matching Ensures transformed signature matches trait
Lifetime correctness Generates correct lifetime annotations
Send compatibility Ensures future meets Send bounds (if required)

Key insight: The #[async_trait] attribute serves different purposes on trait definitions versus implementations. On definitions, it transforms async method signatures into boxed future return types, making async methods representable in traits—a feature Rust doesn't yet support natively. On implementations, it generates the boxing code that wraps async bodies to match the transformed trait signatures. Both positions must have the attribute because the transformation must be symmetric: the trait's transformed signature must match the impl's transformed method bodies. The ?Send variant enables non-Send futures for single-threaded contexts where Rc, RefCell, and other non-Send types can be used inside async methods. This macro is a stopgap until Rust gains native async trait support, which will remove the need for boxing overhead and complex lifetime annotations.