Loading pageā¦
Rust walkthroughs
Loading pageā¦
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.
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.
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.
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.
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.
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 futuresThe definition side transforms all method signatures uniformly.
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 annotationsThe implementation side wraps method bodies in pinned boxes.
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.
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.
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 correctlyGeneric methods work with proper bounds propagation.
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.
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 correctlyWhere clauses are preserved in the transformation.
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.
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.
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.
| 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 |
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.
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.
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.