Introduction

When using async methods in trait definitions (via the async-trait crate or native async fn in trait in Rust 1.75+), the compiler may report that the generated future does not implement Send. This prevents the async method from being used with multi-threaded executors like Tokio, which require Send futures to move tasks between threads.

Symptoms

  • future cannot be sent between threads safely
  • within 'impl Future', the trait Send is not implemented for Rc<...>
  • Works with #[tokio::main(flavor = "current_thread")] but fails with default runtime
  • Error points to a trait method that captures Rc, RefCell, or raw pointers
  • async_trait generated code contains non-Send types

Common Causes

  • Capturing Rc instead of Arc inside async trait method
  • Using RefCell instead of Mutex or RwLock
  • Capturing raw pointers in async blocks
  • Using !Send types from external crates
  • Closure captures a !Send reference across await points

Step-by-Step Fix

  1. 1.Replace Rc with Arc for shared ownership:
  2. 2.```rust
  3. 3.// Before: Rc is not Send
  4. 4.use std::rc::Rc;

#[async_trait::async_trait] impl MyTrait for MyService { async fn process(&self) -> Result<(), Error> { let counter = Rc::new(std::cell::Cell::new(0)); // !Send some_async_call().await; counter.set(1); // Captures Rc across await Ok(()) } }

// After: Arc + atomic types are Send use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering};

#[async_trait::async_trait] impl MyTrait for MyService { async fn process(&self) -> Result<(), Error> { let counter = Arc::new(AtomicUsize::new(0)); // Send some_async_call().await; counter.store(1, Ordering::SeqCst); Ok(()) } } ```

  1. 1.Replace RefCell with async-aware Mutex:
  2. 2.```rust
  3. 3.// Before
  4. 4.use std::cell::RefCell;
  5. 5.let data = Rc::new(RefCell::new(Vec::new()));

// After use tokio::sync::Mutex; let data = Arc::new(Mutex::new(Vec::new()));

// Usage in async trait async fn process(&self) -> Result<(), Error> { let mut guard = self.data.lock().await; guard.push("item".to_string()); Ok(()) } ```

  1. 1.**Use #[async_trait(?Send)] when Send is not required**:
  2. 2.```rust
  3. 3.// If the trait genuinely does not need to be Send:
  4. 4.#[async_trait::async_trait(?Send)]
  5. 5.trait LocalProcessor {
  6. 6.async fn process(&self) -> Result<(), Error>;
  7. 7.}

// Then use with single-threaded runtime #[tokio::main(flavor = "current_thread")] async fn main() { let proc = LocalProcessorImpl; proc.process().await.unwrap(); } ```

  1. 1.Avoid capturing non-Send values across await points:
  2. 2.```rust
  3. 3.// Before: captures &mut local across await
  4. 4.async fn handle(&self) -> Result<(), Error> {
  5. 5.let mut buffer = String::new();
  6. 6.self.reader.read_to_string(&mut buffer).await?; // buffer captured
  7. 7.self.parse(&buffer)
  8. 8.}

// After: restructure to avoid capture async fn handle(&self) -> Result<(), Error> { let buffer = self.reader.read_to_string().await?; // Owned, Send self.parse(&buffer) } ```

Prevention

  • Use Arc and Mutex instead of Rc and RefCell in async code
  • Enable Clippy's future_not_send lint: cargo clippy -- -W clippy::future_not_send
  • Use static_assertions::assert_impl_all!(Type: Send) to verify types
  • Prefer owned types over references in async trait implementations
  • Document which traits require Send and which do not
  • Use tokio::sync::Mutex instead of std::sync::Mutex when holding across await