The error you'll meet on day three of async Rust
You've been having a fine time with Tokio. You wrote an async function. You called tokio::spawn on it. The compiler shoves a wall of red text at you that ends with something like:
error: future cannot be sent between threads safely
--> src/main.rs:14:5
|
14 | tokio::spawn(my_task());
| ^^^^^^^^^^^^ future returned by `my_task` is not `Send`
|
note: future is not `Send` as this value is used across an await
--> src/main.rs:8:9
|
7 | let rc = Rc::new(42);
8 | do_something().await;
| ^^^^^^^^^^ await occurs here, with `rc` maybe used later
= note: `Rc<i32>` cannot be sent between threads safely
The first time this happens, the message is bewildering. "I'm not sending anything between threads. I just spawned a task." The trick is understanding what tokio::spawn actually requires, and why some types make a whole future unsendable.
What Send and Sync mean, briefly
Rust has two marker traits that govern multi-threading. Send means "this type is safe to move to another thread." Sync means "this type is safe to share between threads via &T." Most types implement both automatically. A few don't, by design: Rc<T>, RefCell<T>, Cell<T>, raw pointers, and anything that holds them. Those types use non-atomic operations or interior mutability that would be a data race across threads.
Now, tokio::spawn takes a future and parks it onto Tokio's thread pool. The pool runs the future, possibly on a different worker thread each time it wakes up after an .await. So Tokio needs to be able to move the future across threads. That's why it requires Future + Send + 'static.
Here's the slightly subtle part. The future's Send-ness depends on every piece of state it captures. When the compiler turns your async block into a state machine, every variable that stays alive across an .await ends up as a field in that state machine. If any of those variables are not Send, the whole state machine is not Send, and Tokio refuses it.
Reproducing the error
Let's write the buggy version first. We'll use Rc<T>, which is single-thread reference counting and famously not Send.
use std::rc::Rc;
async fn do_something() {
// Some other async work.
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
async fn my_task() {
// Rc is the troublemaker. It is NOT Send.
let rc = Rc::new(42);
// The await splits this function into a state machine.
// Everything alive across the await ends up in that state machine,
// including `rc`.
do_something().await;
println!("rc = {}", rc);
}
#[tokio::main]
async fn main() {
// Tokio needs the future to be Send so it can move it between worker threads.
// But our future contains an Rc, which kills Send. Compiler refuses.
tokio::spawn(my_task()).await.unwrap();
}
Compile and you'll see the same error you saw before. The compiler points at the await because that's where the state machine boundary is, and at the offending type (Rc<i32>).
The two-step fix
The fix is conceptual: anything that lives across .await inside a spawned task has to be Send. You have two ways to make that true.
Option one. Don't keep the troublemaker alive across the await. If rc is dropped before the .await, it never enters the state machine.
async fn my_task_fixed_a() {
let value = {
// rc lives only inside this block. It's dropped at the closing brace.
let rc = std::rc::Rc::new(42);
*rc
};
// After the await, rc is gone. The future has no non-Send state here.
do_something().await;
println!("value = {}", value);
}
Option two. Use a thread-safe alternative. Arc<T> is the cross-thread version of Rc<T>. Same API, atomic counter. Mutex<T> lives in std::sync (or tokio::sync for async-aware). RwLock similarly. Swap the type and you're done.
use std::sync::Arc;
async fn my_task_fixed_b() {
// Arc is Send + Sync because the counter is atomic.
let arc = Arc::new(42);
do_something().await;
println!("arc = {}", arc);
}
For shared mutable state across an await, the canonical pair is Arc<Mutex<T>>. But notice: a regular std::sync::Mutex guard (MutexGuard) is also a problem if you hold it across an .await, because the guard isn't Send on some platforms and, even where it is, holding a sync mutex across an await is a recipe for deadlock and high latency. The async-aware version is tokio::sync::Mutex, whose guard is designed to be held across awaits.
use std::sync::Arc;
use tokio::sync::Mutex;
async fn updater(state: Arc<Mutex<Vec<i32>>>) {
// tokio::sync::Mutex returns a guard you CAN hold across an await.
let mut guard = state.lock().await;
guard.push(1);
// It's still a good idea to drop the guard before any long-running awaits,
// but at least the type system won't bite you.
do_something().await;
guard.push(2);
}
When the offender is hidden
Sometimes you don't see an Rc or a RefCell anywhere in your code, and you still get the error. That usually means a library type contains one. A few common offenders:
*const Tand*mut T(raw pointers). Ban hammer.MutexGuardfromstd::sync::Mutex, in some cases.- Some FFI handles wrap raw pointers and are not
Sendby default. - A struct you wrote that contains any of the above.
The compiler error usually names the inner type, but the trail can be a few hops long. Read the second note: in the error carefully. It will say something like Rc<i32>` cannot be sent between threads safely` or *const u8 cannot be sent between threads safely. That's your real culprit.
If a type from a library is not Send but you're certain you can use it safely, you have two unsafe escape hatches. Wrap it in your own newtype and unsafe impl Send for MyWrapper {}, or use tokio::task::spawn_local with a LocalSet so the future runs on a single thread and never needs to be Send. We'll get to that next.
When you genuinely don't want to send
tokio::spawn requires Send because it can move work across threads. There's a sibling, tokio::task::spawn_local, that runs work on the current thread inside a LocalSet. Futures spawned with spawn_local don't need to be Send. This is the right call when your code legitimately uses Rc, RefCell, or anything else single-threaded, and you don't need parallelism, just concurrency on one thread.
#[tokio::main(flavor = "current_thread")]
async fn main() {
let local = tokio::task::LocalSet::new();
local.run_until(async {
tokio::task::spawn_local(my_task()).await.unwrap();
}).await;
}
That works because everything is happening on one thread. The future never crosses a thread boundary, so the Send bound is dropped.
You're trading throughput for type freedom. That's often the right trade for, say, a UI thread or a single-threaded simulation. Don't reach for it just to silence the compiler. The compiler is usually telling you something true: if you tokio::spawn an Rc, your program would actually be wrong.
Holding a sync MutexGuard across .await
This deserves its own subsection because it's the form most likely to bite real programs. You write:
use std::sync::Mutex;
async fn bad(state: std::sync::Arc<Mutex<i32>>) {
let mut guard = state.lock().unwrap();
*guard += 1;
// Bad. The guard is alive across this await.
do_something().await;
*guard += 1;
}
Two problems. First, on some platforms the MutexGuard is !Send, so you'll get the original error if you tokio::spawn this. Second and more importantly, even where it compiles, you've turned a sync mutex into a potential deadlock. Worker thread A holds the mutex, suspends at .await, gets parked. Worker thread B picks up another future that needs the same mutex. B blocks the entire worker thread waiting for A. Tokio runs out of workers. Everything stalls.
Two cures. Either drop the guard before the await, or switch to tokio::sync::Mutex:
async fn good_a(state: std::sync::Arc<std::sync::Mutex<i32>>) {
{
let mut guard = state.lock().unwrap();
*guard += 1;
} // guard dropped here, before the await
do_something().await;
}
async fn good_b(state: std::sync::Arc<tokio::sync::Mutex<i32>>) {
let mut guard = state.lock().await;
*guard += 1;
do_something().await;
*guard += 1;
}
tokio::sync::Mutex is slower than std::sync::Mutex for uncontended access, but it integrates with the runtime and its guard is Send. Use it when you know you'll cross awaits while holding the lock. Otherwise prefer the standard mutex with the guard scoped tightly.
A diagnostic checklist
When the error appears, walk this list:
- Read the compiler note that names the non-
Sendtype. That's your suspect. - Find where that type appears in scope across an
.await. - Decide: can I drop it before the await? If yes, scope it.
- If I need it across the await, can I swap it for the thread-safe equivalent?
RctoArc,std::sync::Mutexguard totokio::sync::Mutexguard. - If neither works (unusual, but real), do I actually need
tokio::spawn?tokio::task::spawn_localmay be the right call.
Most uses of the error are one of the first three. Number five is for when the design genuinely calls for single-threaded async, and recognizing that is itself useful.
Where to go next
The future-not-Send error is one of the few compile errors in Rust that teaches you something deep about how the runtime works. Once you internalize "every variable across an .await is part of a state machine," the rest of async Rust gets a lot less mysterious.