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 safely
  • dyn Future<Output = T> + Send required but !Send found
  • Works on single-threaded runtime but fails on multi-threaded
  • Rc<T> or RefCell<T> captured in async trait method body
  • *mut T raw pointer makes future non-Send

Common Causes

  • Rc or RefCell captured in async method body
  • Raw pointers (*const T, *mut T) in async body
  • Non-Send types from FFI or C libraries
  • !Send types held in struct fields accessed in async method
  • async-trait default Send bound too restrictive

Step-by-Step Fix

  1. 1.Replace Rc/RefCell with Arc/Mutex for thread safety:
  2. 2.```rust
  3. 3.use std::sync::Arc;
  4. 4.use tokio::sync::Mutex;
  5. 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. 1.Use async_trait with ?Send for single-threaded contexts:
  2. 2.```rust
  3. 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. 1.Clone non-Send data before entering async context:
  2. 2.```rust
  3. 3.#[async_trait]
  4. 4.trait DataFetcher {
  5. 5.async fn fetch(&self) -> Result<Data>;
  6. 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. 1.Verify Send bound with static assertion:
  2. 2.```rust
  3. 3.// Compile-time check that a type is Send
  4. 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 Arc instead of Rc and Mutex/RwLock instead of RefCell
  • 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_send lint to catch missing Send bounds
  • Test code on multi-threaded runtime to catch Send issues early