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 safelywithin '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_traitgenerated code contains non-Send types
Common Causes
- Capturing
Rcinstead ofArcinside async trait method - Using
RefCellinstead ofMutexorRwLock - Capturing raw pointers in async blocks
- Using
!Sendtypes from external crates - Closure captures a
!Sendreference across await points
Step-by-Step Fix
- 1.Replace Rc with Arc for shared ownership:
- 2.```rust
- 3.// Before: Rc is not Send
- 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.Replace RefCell with async-aware Mutex:
- 2.```rust
- 3.// Before
- 4.use std::cell::RefCell;
- 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.**Use
#[async_trait(?Send)]when Send is not required**: - 2.```rust
- 3.// If the trait genuinely does not need to be Send:
- 4.#[async_trait::async_trait(?Send)]
- 5.trait LocalProcessor {
- 6.async fn process(&self) -> Result<(), Error>;
- 7.}
// Then use with single-threaded runtime #[tokio::main(flavor = "current_thread")] async fn main() { let proc = LocalProcessorImpl; proc.process().await.unwrap(); } ```
- 1.Avoid capturing non-Send values across await points:
- 2.```rust
- 3.// Before: captures &mut local across await
- 4.async fn handle(&self) -> Result<(), Error> {
- 5.let mut buffer = String::new();
- 6.self.reader.read_to_string(&mut buffer).await?; // buffer captured
- 7.self.parse(&buffer)
- 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
ArcandMutexinstead ofRcandRefCellin async code - Enable Clippy's
future_not_sendlint: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::Mutexinstead ofstd::sync::Mutexwhen holding across await