Introduction
The #[async_trait] macro generates Pin<Box<dyn Future + Send>> by default, requiring the returned future to be Send. When the async body captures non-Send types (like Rc, RefCell, or raw pointers), the compiler rejects the trait implementation. This blocks the use of async traits with tokio::spawn on multi-threaded runtimes, which requires Send futures.
Symptoms
future cannot be sent between threads safelydyn Future<Output = T> + Sendrequired but!Sendfound- Works on single-threaded runtime but fails on multi-threaded
Rc<T>orRefCell<T>captured in async trait method body*mut Traw pointer makes future non-Send
Common Causes
RcorRefCellcaptured in async method body- Raw pointers (
*const T,*mut T) in async body - Non-Send types from FFI or C libraries
!Sendtypes held in struct fields accessed in async methodasync-traitdefaultSendbound too restrictive
Step-by-Step Fix
- 1.Replace Rc/RefCell with Arc/Mutex for thread safety:
- 2.```rust
- 3.use std::sync::Arc;
- 4.use tokio::sync::Mutex;
- 5.use async_trait::async_trait;
// WRONG - Rc is not Send use std::rc::Rc; use std::cell::RefCell;
#[async_trait] trait Processor { async fn process(&self) -> Result<String>; }
struct BadProcessor { data: Rc<RefCell<String>>, // Not Send! }
#[async_trait] impl Processor for BadProcessor { async fn process(&self) -> Result<String> { let mut data = self.data.borrow_mut(); // Cannot Send data.push_str("processed"); Ok(data.clone()) } }
// CORRECT - Arc + Mutex is Send struct GoodProcessor { data: Arc<Mutex<String>>, // Send! }
#[async_trait] impl Processor for GoodProcessor { async fn process(&self) -> Result<String> { let mut data = self.data.lock().await; // Async-aware, Send data.push_str("processed"); Ok(data.clone()) } }
// Now this works: let processor = Arc::new(GoodProcessor { ... }); tokio::spawn(async move { processor.process().await // OK: Send }); ```
- 1.Use async_trait with ?Send for single-threaded contexts:
- 2.```rust
- 3.use async_trait::async_trait;
// Mark trait as not requiring Send #[async_trait(?Send)] trait LocalProcessor { async fn process(&self) -> Result<String>; }
// Implementation can use non-Send types struct LocalProcessorImpl { data: std::rc::Rc<String>, }
#[async_trait(?Send)] impl LocalProcessor for LocalProcessorImpl { async fn process(&self) -> Result<String> { Ok(self.data.as_ref().clone()) // Rc is fine with ?Send } }
// Use with local spawn (single-threaded) let handle = tokio::task::spawn_local(async move { let processor = LocalProcessorImpl { data: Rc::new("data".into()) }; processor.process().await }); ```
- 1.Clone non-Send data before entering async context:
- 2.```rust
- 3.#[async_trait]
- 4.trait DataFetcher {
- 5.async fn fetch(&self) -> Result<Data>;
- 6.}
struct Fetcher { // Non-Send config config: std::rc::Rc<Config>, }
#[async_trait] impl DataFetcher for Fetcher { async fn fetch(&self) -> Result<Data> { // Clone non-Send data BEFORE any await point let config = (*self.config).clone(); // Clone Rc data into owned value
// Now the async body only uses Send types make_request(&config).await } } ```
- 1.Verify Send bound with static assertion:
- 2.```rust
- 3.// Compile-time check that a type is Send
- 4.fn assert_send<T: Send>() {}
#[tokio::main] async fn main() { // Verify your processor is Send before spawning assert_send::<GoodProcessor>();
// This will fail at compile time if BadProcessor is not Send // assert_send::<BadProcessor>(); // Uncomment to see the error
let processor = Arc::new(GoodProcessor::new()); tokio::spawn(async move { processor.process().await }); } ```
Prevention
- Use
Arcinstead ofRcandMutex/RwLockinstead ofRefCell - Add
fn assert_send<T: Send>() {}as a compile-time check - Clone non-Send data before await points
- Use
#[async_trait(?Send)]only when single-threaded execution is guaranteed - Enable
clippy::future_not_sendlint to catch missing Send bounds - Test code on multi-threaded runtime to catch Send issues early