Loading pageā¦
Rust walkthroughs
Loading pageā¦
async_trait::async_trait handle the Send bound for futures in trait methods?async_trait::async_trait transforms async trait methods into synchronous methods that return a Pin<Box<dyn Future>>, and by default it adds a Send bound to the boxed future, making traits usable across thread boundaries in async runtimes like tokio. The macro achieves this by desugaring async fn into a method returning Pin<Box<dyn Future<Output = T> + Send>>, ensuring the returned future can be spawned on multi-threaded executors. When the Send bound is inappropriateāfor methods that capture Rc or other non-thread-safe typesāthe macro provides #[async_trait(?Send)] to remove the bound, yielding Pin<Box<dyn Future<Output = T>>> without Send. This dual behavior addresses the core challenge: async methods in traits need to express whether their futures are thread-safe, and the macro's default Send bound matches the common case of multi-threaded async runtimes while allowing opt-out for single-threaded contexts.
// This doesn't work in traits before Rust 1.75
trait Database {
async fn get_user(&self, id: u64) -> Option<User>;
// Error: async fn not allowed in traits
// The trait needs to express the return type
}
struct User {
id: u64,
name: String,
}Native async trait methods weren't possible before Rust 1.75 due to how async functions desugar.
use async_trait::async_trait;
#[async_trait]
trait Database {
async fn get_user(&self, id: u64) -> Option<String>;
}
// The macro transforms this into:
trait Database {
fn get_user<'life0, 'async_trait>(
&'life0 self,
id: u64,
) -> Pin<Box<dyn Future<Output = Option<String>> + Send + 'async_trait>>
where
'life0: 'async_trait,
Self: 'async_trait;
}The async method becomes a synchronous method returning a boxed, pinned, Send future.
use async_trait::async_trait;
use std::pin::Pin;
use std::future::Future;
#[async_trait]
trait Service {
// By default, futures are Send
async fn process(&self, data: String) -> String;
}
struct MyService;
#[async_trait]
impl Service for MyService {
async fn process(&self, data: String) -> String {
data.to_uppercase()
}
}
// The returned future is Pin<Box<dyn Future<Output = String> + Send>>
// This can be spawned on tokio's multi-threaded runtimeThe default Send bound makes futures compatible with multi-threaded executors.
use async_trait::async_trait;
use tokio::task::spawn;
#[async_trait]
trait Worker {
async fn do_work(&self) -> i32;
}
struct MyWorker;
#[async_trait]
impl Worker for MyWorker {
async fn do_work(&self) -> i32 {
42
}
}
#[tokio::main]
async fn main() {
let worker = MyWorker;
// This works because the future is Send
let handle = spawn(async move {
let result = worker.do_work().await;
result
});
let result = handle.await.unwrap();
println!("Result: {}", result);
}The Send bound enables spawning on multi-threaded tokio.
use async_trait::async_trait;
use std::rc::Rc;
#[async_trait(?Send)]
trait LocalService {
// Futures are NOT Send
async fn process(&self) -> String;
}
struct MyLocalService {
// Rc is not Send
data: Rc<String>,
}
#[async_trait(?Send)]
impl LocalService for MyLocalService {
async fn process(&self) -> String {
// Can capture Rc since future doesn't need to be Send
(*self.data).clone()
}
}
// This cannot be spawned on multi-threaded runtime
// Use with tokio::task::spawn_local or single-threaded runtime?Send removes the Send bound for methods that capture non-thread-safe types.
use async_trait::async_trait;
// With Send (default)
#[async_trait]
trait SendTrait {
async fn method(&self) -> i32;
// Expands to:
// fn method(&self) -> Pin<Box<dyn Future<Output = i32> + Send>>
}
// Without Send
#[async_trait(?Send)]
trait LocalTrait {
async fn method(&self) -> i32;
// Expands to:
// fn method(&self) -> Pin<Box<dyn Future<Output = i32>>>
// Note: no Send bound
}
// The difference is in the trait object boundsThe ?Send syntax removes the Send bound from the boxed future.
use async_trait::async_trait;
use std::rc::Rc;
struct NotSendType {
rc_field: Rc<i32>,
}
#[async_trait]
trait Database {
async fn query(&self) -> i32;
}
// This would fail to compile:
// #[async_trait]
// impl Database for NotSendType {
// async fn query(&self) -> i32 {
// *self.rc_field // Error: Rc<i32> is not Send
// }
// }
// Must use ?Send instead:
#[async_trait(?Send)]
trait LocalDatabase {
async fn query(&self) -> i32;
}
#[async_trait(?Send)]
impl LocalDatabase for NotSendType {
async fn query(&self) -> i32 {
*self.rc_field // Works: future doesn't need to be Send
}
}Types containing Rc or other non-Send types require ?Send.
use async_trait::async_trait;
use std::rc::Rc;
trait MixedService {
// Cannot mix Send and ?Send in the same trait
// All methods in a trait share the same bound
}
// Instead, split into separate traits:
#[async_trait]
trait RemoteService {
async fn call_remote(&self) -> String;
}
#[async_trait(?Send)]
trait LocalProcessing {
async fn process_locally(&self, data: Rc<String>) -> String;
}
struct MyService {
remote_data: String,
}
#[async_trait]
impl RemoteService for MyService {
async fn call_remote(&self) -> String {
self.remote_data.clone()
}
}
#[async_trait(?Send)]
impl LocalProcessing for MyService {
async fn process_locally(&self, data: Rc<String>) -> String {
format!("{}-{}", self.remote_data, *data)
}
}A trait must be entirely Send or ?Send; split into multiple traits if needed.
use async_trait::async_trait;
use std::sync::Arc;
// Arc<T> is Send when T: Send + Sync
#[async_trait]
trait ThreadSafeService {
async fn process(&self) -> i32;
}
struct SafeService {
data: Arc<String>, // Arc<String> is Send
}
#[async_trait]
impl ThreadSafeService for SafeService {
async fn process(&self) -> i32 {
self.data.len() as i32
}
}
// This works with default Send boundUsing Arc instead of Rc preserves the Send bound.
use async_trait::async_trait;
#[async_trait]
trait Processor {
async fn process(&self, data: &str) -> String;
}
struct MyProcessor;
#[async_trait]
impl Processor for MyProcessor {
async fn process(&self, data: &str) -> String {
// The future captures the reference
// References to Send types are Send
data.to_uppercase()
}
}
#[tokio::main]
async fn main() {
let processor = MyProcessor;
let input = String::from("hello");
// Works because &str is Send
let result = processor.process(&input).await;
println!("{}", result);
}References to Send types are also Send.
use async_trait::async_trait;
#[async_trait]
trait AsyncIterator {
type Item;
async fn next(&mut self) -> Option<Self::Item>;
}
struct Counter {
count: usize,
max: usize,
}
#[async_trait]
impl AsyncIterator for Counter {
type Item = usize;
async fn next(&mut self) -> Option<Self::Item> {
if self.count < self.max {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
// The generated code includes proper lifetime boundsThe macro handles lifetime bounds automatically for references in method signatures.
use async_trait::async_trait;
use std::rc::Rc;
#[async_trait(?Send)]
trait LocalWorker {
async fn work(&self) -> i32;
}
struct MyWorker {
counter: Rc<i32>, // Rc works with ?Send
}
#[async_trait(?Send)]
impl LocalWorker for MyWorker {
async fn work(&self) -> i32 {
*self.counter
}
}
// Must use single-threaded runtime or spawn_local
// tokio::task::spawn_local requires !Send futures?Send traits work with spawn_local on the current thread.
use async_trait::async_trait;
#[async_trait]
trait Handler: Send + Sync {
async fn handle(&self, request: String) -> String;
}
struct MyHandler;
#[async_trait]
impl Handler for MyHandler {
async fn handle(&self, request: String) -> String {
request.to_uppercase()
}
}
// The trait is object-safe despite async methods
async fn use_handler(handler: &dyn Handler) {
let response = handler.handle("test".to_string()).await;
println!("{}", response);
}
#[tokio::main]
async fn main() {
let handler = MyHandler;
use_handler(&handler).await;
}async_trait makes traits with async methods object-safe through boxing.
use async_trait::async_trait;
use std::fmt::Display;
#[async_trait]
trait Formatter {
async fn format<T: Display + Send + 'static>(&self, value: T) -> String;
}
struct JsonFormatter;
#[async_trait]
impl Formatter for JsonFormatter {
async fn format<T: Display + Send + 'static>(&self, value: T) -> String {
format!("{{\"value\": \"{}\"}}", value)
}
}
#[tokio::main]
async fn main() {
let formatter = JsonFormatter;
let result = formatter.format(42).await;
println!("{}", result);
}Generic parameters require Send bounds to maintain the trait's Send guarantee.
use async_trait::async_trait;
use std::io;
#[async_trait]
trait AsyncReader {
async fn read_all(&self) -> Result<Vec<u8>, io::Error>;
}
struct FileReader {
path: String,
}
#[async_trait]
impl AsyncReader for FileReader {
async fn read_all(&self) -> Result<Vec<u8>, io::Error> {
// Simulate async file read
Ok(self.path.as_bytes().to_vec())
}
}
// Result<..., Error> works naturally with async_traitasync_trait handles complex return types like Result.
// Original code:
#[async_trait]
trait Example {
async fn method(&self) -> i32;
}
// Approximately expands to:
trait Example {
fn method<'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;
}
// Key components:
// 1. Pin<Box<...>> - pinned boxed future
// 2. dyn Future<Output = i32> - trait object for the future
// 3. + Send - the Send bound (default)
// 4. 'async_trait - lifetime boundUnderstanding the expansion helps debug trait bound errors.
use async_trait::async_trait;
use std::rc::Rc;
#[async_trait]
trait Service {
async fn run(&self) -> i32;
}
// Error case: trying to use Rc with Send bound
struct BrokenService {
data: Rc<i32>,
}
// This fails to compile:
// #[async_trait]
// impl Service for BrokenService {
// async fn run(&self) -> i32 {
// *self.data // Rc<i32> cannot be sent between threads
// }
// }
// Fix option 1: Use Arc instead of Rc
use std::sync::Arc;
struct FixedService {
data: Arc<i32>,
}
#[async_trait]
impl Service for FixedService {
async fn run(&self) -> i32 {
*self.data
}
}
// Fix option 2: Use ?Send
#[async_trait(?Send)]
trait LocalService {
async fn run(&self) -> i32;
}
#[async_trait(?Send)]
impl LocalService for BrokenService {
async fn run(&self) -> i32 {
*self.data
}
}When Send bound fails, either change types or use ?Send.
// Rust 1.75+ supports native async fn in traits
// But requires return_position_impl_trait_in_trait feature
// Native (Rust 1.75+):
trait NativeAsync {
async fn method(&self) -> i32;
// Returns impl Future<Output = i32>
// Send bound inferred from implementation
}
// With async_trait:
#[async_trait]
trait MacroAsync {
async fn method(&self) -> i32;
// Returns Pin<Box<dyn Future<Output = i32> + Send>>
// Send bound is explicit and consistent
}
// Key differences:
// - Native: impl Future (no boxing)
// - async_trait: Box<dyn Future> (boxing, dynamic dispatch)
// - Native: Send inferred from body
// - async_trait: Send explicit (or ?Send to disable)Native async traits avoid boxing; async_trait provides explicit Send control.
use async_trait::async_trait;
#[async_trait]
trait FastService {
async fn compute(&self) -> i32;
}
struct MyService;
#[async_trait]
impl FastService for MyService {
async fn compute(&self) -> i32 {
42
}
}
// The boxing in async_trait has overhead:
// 1. Heap allocation for the boxed future
// 2. Dynamic dispatch through dyn Future
// 3. Vtable lookup for poll()
// For hot paths, native async traits (Rust 1.75+) may be faster
// For most applications, the overhead is negligibleBoxing has allocation overhead; native async traits are more efficient.
Send bound behavior:
| Syntax | Future Type | Send Bound |
|--------|-------------|------------|
| #[async_trait] | Pin<Box<dyn Future<Output = T> + Send>> | Yes |
| #[async_trait(?Send)] | Pin<Box<dyn Future<Output = T>>> | No |
When to use each:
| Scenario | Recommended |
|----------|-------------|
| Multi-threaded tokio | #[async_trait] (default Send) |
| Single-threaded runtime | #[async_trait(?Send)] |
| Capturing Rc, RefCell | #[async_trait(?Send)] |
| Object-safe trait with async | #[async_trait] |
| High-performance hot paths | Native async (Rust 1.75+) |
Type requirements for Send futures:
| Type | Send? | Works with default? |
|------|-------|---------------------|
| Arc<T> where T: Send + Sync | Yes | Yes |
| Rc<T> | No | No, use ?Send |
| &T where T: Sync | Yes | Yes |
| &mut T where T: Send | Yes | Yes |
| RefCell<T> | No | No, use ?Send |
| Mutex<T> where T: Send | Yes | Yes |
Key insight: async_trait::async_trait addresses the fundamental challenge of expressing "this async method produces a future that can be sent across threads" in Rust's trait system. By defaulting to Send-bounded futures, the macro aligns with the common case of multi-threaded async runtimes like tokio where spawning tasks requires Send. The #[async_trait(?Send)] opt-out provides an escape hatch for legitimate non-thread-safe contexts: single-threaded runtimes, spawn_local scenarios, or methods that inherently capture Rc or RefCell. The macro's boxing approachāPin<Box<dyn Future>>āis the mechanism that makes this work: it erases the concrete future type into a trait object, allowing the trait to express bounds on the future without knowing its exact shape. This design choice trades some performance (heap allocation, dynamic dispatch) for the ability to write natural async trait methods before Rust 1.75's native support. The explicit Send bound is more than a defaultāit's a contract that implementations must satisfy, ensuring that any dyn Trait can safely be used across threads without the implementor accidentally capturing non-thread-safe state.