How does async_trait::async_trait handle Send bounds for async trait methods?
async_trait::async_trait boxes the returned future from async trait methods as Pin<Box<dyn Future>>, and by default applies a Send bound to make the boxed future Send-safe for use in async runtimes. This allows async trait methods to work with multi-threaded runtimes like tokio, but requires the ?Send opt-out syntax when futures contain non-Send types.
The Core Problem
// Without async_trait, this doesn't work in stable Rust:
trait AsyncService {
async fn process(&self, data: Vec<u8>) -> Vec<u8>;
}
// Error: async fn in trait is not stable for all use cases
// The compiler cannot guarantee the future's size at compile time
// because different implementations may have different future sizesAsync trait methods require boxing to have a known size for the vtable.
What async_trait Generates
use async_trait::async_trait;
// With the async_trait macro:
#[async_trait]
trait AsyncService {
async fn process(&self, data: Vec<u8>) -> Vec<u8>;
}
// The macro transforms the async method into:
trait AsyncService {
fn process<'life0, 'async_trait>(
&'life0 self,
data: Vec<u8>,
) -> Pin<Box<dyn Future<Output = Vec<u8>> + Send + 'async_trait>>
where
Self: Sync + 'async_trait,
'life0: 'async_trait;
}
// The key differences:
// 1. async fn becomes fn returning Pin<Box<dyn Future>>
// 2. The boxed future has + Send bound by default
// 3. Lifetime parameters are added for the async_traitThe macro boxes the future and adds Send bound by default.
Default Send Bound
use async_trait::async_trait;
use std::sync::Arc;
#[async_trait]
trait Database {
async fn query(&self, sql: &str) -> Vec<Row>;
}
struct PostgresDb {
pool: Arc<ConnectionPool>,
}
#[async_trait]
impl Database for PostgresDb {
async fn query(&self, sql: &str) -> Vec<Row> {
// The generated future is Send because:
// 1. self is &PostgresDb (Send)
// 2. sql is &str (Send)
// 3. Row is Send
// 4. ConnectionPool is Send
self.pool.execute(sql).await
}
}
// This works with multi-threaded runtimes:
async fn use_database(db: Arc<dyn Database>) {
// Can spawn on multi-threaded runtime because
// dyn Database::query returns Send future
tokio::spawn(async move {
db.query("SELECT * FROM users").await;
});
}Default Send bound makes async traits compatible with multi-threaded runtimes.
The Send Bound on Boxed Future
use async_trait::async_trait;
#[async_trait]
trait Service {
async fn handle(&self, request: Request) -> Response;
}
// What the macro actually generates for the trait:
trait Service {
fn handle<'life0, 'async_trait>(
&'life0 self,
request: Request,
) -> Pin<Box<dyn Future<Output = Response> + Send + 'async_trait>>
where
Self: Sync + 'async_trait,
'life0: 'async_trait;
}
// Note the: + Send in the boxed future type
// This ensures the returned future can be sent between threadsThe Send bound is applied to the boxed future, not the trait itself.
Non-Send Futures with ?Send
use async_trait::async_trait;
use std::rc::Rc;
// When a future contains non-Send types, use ?Send
#[async_trait]
trait LocalService {
// ?Send opts out of the Send bound
async fn process(&self, data: Rc<[u8]>) -> Vec<u8>;
}
struct LocalProcessor;
#[async_trait]
impl LocalService for LocalProcessor {
async fn process(&self, data: Rc<[u8]>) -> Vec<u8> {
// Rc is not Send, so this future cannot be Send
data.iter().cloned().collect()
}
}
// Without ?Send, this would fail:
// error: `Rc<[u8]>` cannot be sent between threads safelyUse ?Send when the future contains non-Send types like Rc.
The ?Send Syntax Details
use async_trait::async_trait;
#[async_trait]
trait MixedService {
// Default: Send bound applied
async fn send_method(&self) -> i32;
// Opt out: ?Send removes Send bound
async fn local_method(&self) -> i32;
}
// The ?Send in the return type signals to async_trait
// that this method's future does not need to be Send
// Generated code for local_method:
trait MixedService {
// send_method gets Send bound
fn send_method<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = i32> + Send + 'async_trait>>
where
Self: Sync + 'async_trait,
'life0: 'async_trait;
// local_method without Send bound
fn local_method<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = i32> + 'async_trait>> // No + Send
where
Self: 'async_trait,
'life0: 'async_trait;
}Methods with ?Send in return type get boxed futures without the Send bound.
Common Non-Send Types
use async_trait::async_trait;
use std::rc::Rc;
use std::cell::RefCell;
// Types that are NOT Send:
struct NotSendTypes {
rc: Rc<String>, // Rc is !Send
ref_cell: RefCell<i32>, // RefCell is !Send
raw_pointer: *const u8, // Raw pointers are !Send (by default)
}
#[async_trait]
trait Example {
// This REQUIRES ?Send because Rc is used:
async fn use_rc(&self, data: Rc<Vec<u8>>) -> usize {
data.len()
}
// This also requires ?Send because RefCell is used:
async fn use_refcell(&self, cell: RefCell<i32>) -> i32 {
*cell.borrow()
}
}
// Without ?Send, you get errors like:
// `Rc<Vec<u8>>` cannot be sent between threads safely
// the trait `Send` is not implemented for `Rc<Vec<u8>>`Any non-Send type captured by the future requires ?Send.
Self Types and Send Bounds
use async_trait::async_trait;
// The Self type must also be Send-compatible
#[async_trait]
trait Handler {
async fn handle(&self);
}
// For Send methods, Self must be Send-safe:
struct SendHandler {
data: Vec<u8>, // Send
}
#[async_trait]
impl Handler for SendHandler {
async fn handle(&self) {
// Works: SendHandler is Send
}
}
// Non-Send self types require ?Send:
struct NotSendHandler {
data: Rc<Vec<u8>>, // !Send
}
#[async_trait]
impl Handler for NotSendHandler {
async fn handle(&self) {
// Without ?Send on the trait method:
// error: `NotSendHandler` cannot be sent between threads safely
}
}
// Alternative: define trait with ?Send methods for non-Send types
#[async_trait]
trait LocalHandler {
async fn handle(&self); // No Send requirement
}The implementing type must be Send if the trait method requires Send.
Sync Requirements
use async_trait::async_trait;
// For Send methods, the trait also requires Self: Sync
#[async_trait]
trait Service {
async fn serve(&self);
}
// Generated code includes:
// where Self: Sync + 'async_trait
// This is because &self is borrowed across await points
// The reference must be valid across threads
struct NonSyncService {
cell: std::cell::Cell<i32>, // Cell is !Sync
}
#[async_trait]
impl Service for NonSyncService {
async fn serve(&self) {
// Error: `NonSyncService` cannot be shared between threads safely
// the trait `Sync` is not implemented
}
}
// Solutions:
// 1. Use ?Send to remove Sync requirement
// 2. Use Sync types (AtomicI32 instead of Cell)
// 3. Use Mutex for interior mutabilitySend methods also require Self: Sync because &self lives across await points.
Interior Mutability Patterns
use async_trait::async_trait;
use std::sync::Mutex;
use std::cell::RefCell;
#[async_trait]
trait Counter {
async fn increment(&self) -> i32;
}
// Send-compatible: use Mutex (Sync + Send)
struct SendCounter {
count: Mutex<i32>,
}
#[async_trait]
impl Counter for SendCounter {
async fn increment(&self) -> i32 {
let mut guard = self.count.lock().unwrap();
*guard += 1;
*guard
}
}
// Non-Send: use RefCell (Sync but not Send-safe across threads)
struct LocalCounter {
count: RefCell<i32>,
}
// This REQUIRES ?Send because RefCell is in the future
#[async_trait]
impl Counter for LocalCounter {
async fn increment(&self) -> i32 {
// Error without ?Send: RefCell is not Send-safe
let mut count = self.count.borrow_mut();
*count += 1;
*count
}
}Use Mutex for Send-compatible interior mutability; RefCell requires ?Send.
Runtime Compatibility
use async_trait::async_trait;
use tokio::task::spawn;
#[async_trait]
trait Task {
async fn run(&self);
}
async fn run_on_runtime<T: Task + Send + Sync>(task: T) {
// tokio::spawn requires Send future
spawn(async move {
task.run().await;
});
}
// This works because:
// - T: Send + Sync (the implementing type)
// - Task::run returns Send future (default)
// - The spawned future is Send
// With ?Send:
#[async_trait]
trait LocalTask {
async fn run(&self); // No Send bound
}
async fn run_local<T: LocalTask>(task: T) {
// Cannot use tokio::spawn - the future is not Send
// spawn_local(task.run()); // Would need local runtime
// Must run in place:
task.run().await;
}Send futures work with tokio::spawn; ?Send futures require local executors.
Boxed Future Size Impact
use async_trait::async_trait;
// Boxing has performance implications:
trait Service {
async fn process(&self, data: Vec<u8>) -> Vec<u8>;
}
// Generated code boxes the entire future:
impl Service for MyService {
fn process<'life0, 'async_trait>(
&'life0 self,
data: Vec<u8>,
) -> Pin<Box<dyn Future<Output = Vec<u8>> + Send + 'async_trait>>
where
Self: Sync + 'async_trait,
'life0: 'async_trait,
{
Box::pin(async move {
// Entire async block is boxed
// Allocation on every call
self.do_process(data).await
})
}
}
// Performance impact:
// 1. Heap allocation per call
// 2. Dynamic dispatch for poll()
// 3. Extra indirection
// 4. But: stable Rust compatibility
// Alternative (when stable): return impl Future
// trait Service {
// fn process(&self, data: Vec<u8>) -> impl Future<Output = Vec<u8>>;
// }
// This doesn't work for trait objects (dyn Service)Boxing enables trait object compatibility at the cost of allocation.
Generic Send Bounds
use async_trait::async_trait;
#[async_trait]
trait Repository<T> {
async fn get(&self, id: u64) -> Option<T>;
}
// Generic implementations:
struct Cache<T> {
data: Vec<T>,
}
#[async_trait]
impl<T: Send + Sync + 'static> Repository<T> for Cache<T> {
async fn get(&self, id: u64) -> Option<T> {
// T must be Send + Sync for the future to be Send
self.data.get(id as usize).cloned()
}
}
// If T is not Send:
struct NotSendType {
rc: std::rc::Rc<()>,
}
// This won't work:
// impl Repository<NotSendType> for Cache<NotSendType> { ... }
// Error: NotSendType does not implement Send
// Solution: use ?Send on the traitGeneric types must be Send for default async trait methods.
Conditional Send Bounds
use async_trait::async_trait;
// Sometimes you want Send conditionally
#[async_trait]
trait ConditionalService {
// This doesn't work directly - async_trait doesn't support
// conditional Send bounds on individual methods
// Alternative: define two traits
// For Send futures (multi-threaded)
async fn send_process(&self);
// For local futures (single-threaded)
async fn local_process(&self);
}
// Or use generic parameters:
#[async_trait]
trait GenericService<S: Send> {
async fn process(&self, data: S) -> S;
}Complex Send conditions may require separate traits or redesign.
Comparison Table
use async_trait::async_trait;
fn comparison_table() {
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// β Aspect β Send Method β ?Send Method β
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
// β Future bound β Pin<Box<dyn Future + Send>> β Pin<Box<dyn Future>>β
// β Self requirement β Self: Sync β No Sync requirement β
// β tokio::spawn β Compatible β Not compatible β
// β Runtime β Multi-threaded β Single-threaded β
// β Rc/RefCell β Not allowed β Allowed β
// β Cross-thread safety β Required β Not required β
// β Performance β Same (boxed) β Same (boxed) β
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
}
// Default behavior: Send (multi-threaded compatible)
// Use ?Send when: Rc, RefCell, or other !Send types are neededSend methods are the default for multi-threaded compatibility.
Complete Summary
use async_trait::async_trait;
#[async_trait]
trait CompleteSummary {
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// β What async_trait does: β
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
// β 1. Transforms async fn into fn returning Pin<Box<dyn Future>> β
// β 2. Adds Send bound to boxed future by default β
// β 3. Requires Self: Sync for Send methods (reference across await) β
// β 4. Supports ?Send opt-out for non-Send futures β
// β 5. Enables trait objects with async methods β
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
}
// Key behaviors:
//
// Default (Send bound):
// - async fn method() -> T;
// - Generates: Pin<Box<dyn Future<Output = T> + Send>>
// - Self must be Sync
// - Works with tokio::spawn
// - Multi-threaded runtime compatible
//
// With ?Send:
// - async fn method() -> T;
// - Generates: Pin<Box<dyn Future<Output = T>>> (no Send)
// - No Sync requirement
// - Cannot use tokio::spawn
// - Single-threaded or local runtime only
//
// Use ?Send when:
// - Capturing Rc<T>
// - Capturing RefCell<T>
// - Capturing *const T or *mut T
// - Self type is not Sync
// - Any captured type is !Send
//
// The Send bound ensures futures work with multi-threaded runtimes
// but restricts what types can be used in the async block.Key insight: async_trait::async_trait boxes async method futures with a Send bound by default, making them compatible with multi-threaded runtimes like tokio. The Send bound requires that all captured types are Send and Self is Sync. When using non-Send types like Rc or RefCell, use ?Send in the return type to opt out of the Send requirementβbut then the future cannot be spawned on multi-threaded runtimes. The trade-off is between multi-threaded compatibility (default Send) and single-threaded flexibility (?Send).
