Error

"the trait Send is not implemented for (some async type)" — How to Fix

Fix the 'trait Send is not implemented' error by ensuring all struct fields are thread-safe or wrapping them in Arc and Mutex.

The async block that won't spawn

You write an async function to fetch data. You pass it to tokio::spawn. The compiler rejects you with error[E0277]: the trait bound {async block}: Send is not satisfied. You stare at the code. It looks fine. You're just moving a struct into a closure. Why does Rust think your data is radioactive?

This error appears when you try to run an async task on a multi-threaded executor, but the task captures a type that cannot safely move across threads. The compiler blocks you because moving that type would risk data races, memory corruption, or undefined behavior. The fix is almost always swapping a single-threaded type for a thread-safe alternative, or restructuring how you capture data.

What Send actually means

Send is a marker trait. It tells the compiler: "This type is safe to transfer ownership to another thread." Most types are Send automatically. Integers, strings, vectors, and Arc<T> all implement Send. They rely on memory layouts or synchronization primitives that work correctly when moved between threads.

Some types are not Send. Rc<T> is the classic offender. RefCell<T> is another. Raw pointers and Cell<T> also fail the check. These types rely on thread-local assumptions or lack synchronization. Rc<T> uses a plain integer for its reference count. If two threads increment that integer simultaneously, the count gets corrupted. Memory leaks or double-frees follow. Rust marks Rc<T> as !Send to prevent this.

The compiler derives Send for your structs recursively. If your struct contains a field that is !Send, the whole struct is !Send. The trait bound propagates up the chain. You cannot opt out of this check. The compiler enforces it to guarantee thread safety.

Why async code demands Send

Async functions and blocks compile into state machines. These state machines capture every variable you use inside the async body. When you write async move { ... }, the generated struct holds all captured values. The executor runs these state machines.

Multi-threaded executors like Tokio's default runtime move tasks between worker threads. When a task yields at an .await point, the executor parks it. When the task becomes ready, the executor schedules it on whatever thread is available. That thread might be different from the one where the task started.

If the task holds a !Send value, moving it to another thread violates the type's invariants. The executor requires all spawned tasks to implement Send. This ensures the executor can move tasks freely without risking safety. The trait bound Send is a contract between your code and the runtime. The runtime promises to move the task. Your code promises the task can survive the move.

Single-threaded executors do not require Send. If you use tokio::runtime::Builder::new_current_thread(), tasks never leave the current thread. You can spawn non-Send tasks there. This is useful for single-threaded tools or when you need to use types like Rc<T> in async code. The trade-off is that you lose parallelism. All tasks run on one thread.

Minimal example: Rc vs Arc

The most common trigger is using Rc<T> in a multi-threaded context. Rc<T> provides shared ownership without thread safety. Arc<T> provides shared ownership with thread safety. Arc stands for "Atomic Reference Counted." It uses atomic operations to update the reference count, which are safe for concurrent access.

use std::rc::Rc;
use tokio::task;

/// Demonstrates the Send error with Rc.
#[tokio::main]
async fn main() {
    // Rc is not Send. It uses a non-atomic counter.
    let data = Rc::new(vec![1, 2, 3]);

    // spawn requires the future to be Send.
    // This fails with E0277.
    task::spawn(async move {
        println!("{:?}", data);
    });
}

The compiler stops you with E0277. It points to the spawn call. The message says the async block doesn't implement Send. The root cause is Rc. The error chain shows Rc<Vec<i32>> is not Send. The fix is to replace Rc with Arc.

use std::sync::Arc;
use tokio::task;

/// Demonstrates the fix using Arc.
#[tokio::main]
async fn main() {
    // Arc is Send. It uses atomic operations for the counter.
    let data = Arc::new(vec![1, 2, 3]);

    // Arc::clone is the convention for cloning Arc.
    // It signals a shallow clone, not a deep copy.
    let data_clone = Arc::clone(&data);

    task::spawn(async move {
        println!("{:?}", data_clone);
    });
}

Arc::clone(&data) is the community convention. Both Arc::clone(&data) and data.clone() compile and work identically. The explicit form is preferred because data.clone() looks like a deep clone to readers familiar with other languages. The explicit form signals that you are cloning the pointer, not the data.

Realistic trap: The MutexGuard capture

A subtle trap occurs when you lock a mutex and try to spawn a task while holding the lock. MutexGuard is not Send. The guard holds a reference to the mutex and tracks whether the lock is held. Unlocking happens in the destructor. If the guard moves to another thread, it unlocks on that thread. The lock was acquired on thread A, unlocked on thread B. This is undefined behavior for some mutex implementations and a logic error for others. Rust marks MutexGuard as !Send.

use std::sync::Mutex;
use tokio::task;

struct SharedState {
    value: Mutex<usize>,
}

impl SharedState {
    /// Attempts to spawn work while holding a lock.
    fn process(&self) {
        // Lock the mutex.
        let guard = self.value.lock().unwrap();

        // Try to spawn work while holding the lock.
        // This fails because MutexGuard is not Send.
        task::spawn(async move {
            println!("Processing {}", guard);
        });
    }
}

The error points to MutexGuard. The fix is to avoid capturing the guard. Clone the data or restructure the code so the lock is acquired inside the task. If you need shared ownership, wrap the mutex in Arc.

use std::sync::{Arc, Mutex};
use tokio::task;

struct SharedState {
    value: Arc<Mutex<usize>>,
}

impl SharedState {
    /// Spawns work by cloning the Arc and locking inside the task.
    fn process(&self) {
        // Clone the Arc, not the guard.
        let value = Arc::clone(&self.value);

        task::spawn(async move {
            // Lock inside the task.
            // The guard lives and dies on the worker thread.
            let guard = value.lock().unwrap();
            println!("Processing {}", guard);
        });
    }
}

Lock inside the task. Capturing a guard is a deadlock waiting to happen. The guard prevents other threads from accessing the data, and the async task might yield, blocking progress indefinitely.

Debugging the error chain

The compiler error can be long. It lists the trait bounds that failed. Look for the chain of "required for" messages. The chain traces from the async block back to the leaf type that is !Send.

The error often looks like this:

error[E0277]: the trait bound {async block@...}: Send is not satisfied
  --> src/main.rs:10:5
   |
10 |     task::spawn(async move {
   |     ^^^^^^^^^^^ the trait `Send` is not implemented for `Rc<Vec<i32>>`
   |
   = help: the trait `Send` is required because this argument is evaluated in a spawn call
   = note: required for `std::rc::Rc<Vec<i32>>` to implement `Send`

The key line is "the trait Send is not implemented for Rc<Vec<i32>>". This identifies the culprit. Replace Rc with Arc. If the error mentions RefCell, replace it with Mutex or RwLock. If it mentions Cell, use an atomic type.

Sometimes the error points to a type you didn't expect. Closures capture variables by reference or move. If you capture &self and self contains a !Send field, the closure isn't Send. Use move to take ownership, or clone the specific data you need. Avoid capturing large structs by reference when spawning tasks. Clone the data or use Arc to share it.

Convention aside: let _ = ... to discard a result is a signal to readers that you considered the value and chose to drop it. When debugging, you might see let _ = spawn(...) in examples. This suppresses the warning about unused results. It does not fix Send errors. The error happens at the call site, not the result.

Decision: Choosing thread-safe types

Use Arc<T> when you need shared ownership across threads. It is the thread-safe version of Rc<T>. Use Mutex<T> or RwLock<T> when multiple threads need to mutate shared data safely. RefCell<T> is single-threaded only. Use atomic types like AtomicUsize or AtomicBool when you need lock-free updates for simple values. Use spawn_blocking when you call a blocking library that returns non-Send types or holds locks across boundaries. Use a current-thread runtime when you are building a single-threaded tool and want to avoid the overhead of thread safety.

Thread safety is a contract. Sign it with Send. Swap the type, not the compiler.

Where to go next