implementation of `Send` is not general enough
ID: rust/send-not-general-enough
Version Compatibility
| Version | Status | Introduced | Deprecated | Notes |
|---|---|---|---|---|
| 1 | active | — | — | — |
Root Cause
The Rust compiler determines that a future is not Send because it holds a reference with a non-'static lifetime across an .await point. The Send implementation for the future requires all captured references to be valid for any lifetime, but the compiler can only prove the reference is valid for a specific, shorter lifetime. This commonly occurs in async functions that borrow local data across await points, use closures capturing references in spawned tasks, or pass non-'static references to async trait methods. The error message is notoriously confusing as it discusses higher-ranked trait bounds internally.
genericWorkarounds
-
90% success Clone or move owned data into the async task instead of borrowing
// Before (error): borrowing across spawn async fn process(data: &str) { tokio::spawn(async { println!("{data}"); // borrows non-'static reference }).await.unwrap(); } // After: clone into an owned String and move it async fn process(data: &str) { let owned = data.to_string(); // Clone to owned String tokio::spawn(async move { println!("{owned}"); // owned data, 'static + Send }).await.unwrap(); }Sources: https://tokio.rs/tokio/tutorial/spawning#send-bound
-
88% success Use Arc<T> to share owned data across async tasks
use std::sync::Arc; struct AppState { db_url: String, config: Config, } async fn handle(state: Arc<AppState>) { let state = state.clone(); // Cheap Arc clone tokio::spawn(async move { // state is Arc<AppState>: 'static + Send + Clone println!("DB: {}", state.db_url); }).await.unwrap(); } -
80% success Restructure to avoid spawning by using join! or select! for concurrent work
use tokio::join; // Instead of spawning (which requires Send + 'static): async fn process(data: &str) { // join! runs futures concurrently on the SAME task // No Send or 'static requirement let (result_a, result_b) = join!( fetch_a(data), fetch_b(data), ); println!("{result_a:?}, {result_b:?}"); } async fn fetch_a(data: &str) -> String { format!("a: {data}") } async fn fetch_b(data: &str) -> String { format!("b: {data}") }
Dead Ends
Common approaches that don't work:
-
Add explicit lifetime annotations to the async function signature
85% fail
The issue is not about naming lifetimes -- it is about the async runtime (tokio::spawn) requiring 'static bounds on futures. Adding lifetime annotations to the async fn does not make the future Send + 'static. The spawned task must own all its data because it may outlive the scope that created it. Lifetime annotations only describe relationships; they do not extend data lifetimes.
-
Wrap the borrowed reference in Arc to make it 'static
75% fail
Arc<&'a T> is Send only if &'a T is Send, but the lifetime 'a is still not 'static. Wrapping a reference in Arc does not extend the referent's lifetime. You need Arc<T> (owning the data), not Arc<&T> (sharing a reference). This is a common mistake that produces the same or a similar error.