rust async_error ai_generated true

implementation of `Send` is not general enough

ID: rust/send-not-general-enough

Also available as: JSON · Markdown
75%Fix Rate
80%Confidence
55Evidence
2020-01-01First Seen

Version Compatibility

VersionStatusIntroducedDeprecatedNotes
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.

generic

Workarounds

  1. 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

  2. 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();
    }

    Sources: https://tokio.rs/tokio/tutorial/shared-state

  3. 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}")
    }

    Sources: https://docs.rs/tokio/latest/tokio/macro.join.html

Dead Ends

Common approaches that don't work:

  1. 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.

  2. 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.

Error Chain

Leads to:
Preceded by:
Frequently confused with: