Use when the user hits "future cannot be sent between threads safely", "future is not Send", Pin/Unpin errors, lifetime errors inside an `async fn`, "cannot move out of pinned", or AFIT dyn-dispatch problems. Prevents holding a MutexGuard or Rc across `.await`, storing a non-Unpin future on the stack wrongly, and reflexively reaching for `async_trait` when AFIT now works. Covers: Future not Send (holding non-Send across .await: MutexGuard, Rc, RefCell::Ref), cannot-move-out-of-pinned, !Unpin future on stack, lifetime errors in async fn signatures, async fn in trait dyn-dispatch limits (when async_trait crate is still needed), tokio blocking-in-async runtime panics, block_on inside a running runtime; fix patterns. Keywords: "future is not Send", "future cannot be sent between threads", "cannot be sent between threads safely", "held across an await point", "MutexGuard across await", Pin, Unpin, "cannot move out of", "not Unpin", "async lifetime", "async fn in trait", AFIT, async_trait, "block_on", "blocking the
Use when the user hits "future cannot be sent between threads safely", "future is not Send", Pin/Unpin errors, lifetime errors inside an `async fn`, "cannot move out of pinned", or AFIT dyn-dispatch problems. Prevents holding a MutexGuard or Rc across `.await`, storing a non-Unpin future on the stack wrongly, and reflexively reaching for `async_trait` when AFIT now works. Covers: Future not Send (holding non-Send across .await: MutexGuard, Rc, RefCell::Ref), cannot-move-out-of-pinned, !Unpin future on stack, lifetime errors in async fn signatures, async fn in trait dyn-dispatch limits (when async_trait crate is still needed), tokio blocking-in-async runtime panics, block_on inside a running runtime; fix patterns. Keywords: "future is not Send", "future cannot be sent between threads", "cannot be sent between threads safely", "held across an await point", "MutexGuard across await", Pin, Unpin, "cannot move out of", "not Unpin", "async lifetime", "async fn in trait", AFIT, async_trait, "block_on", "blocking the runtime", "Cannot start a runtime from within a runtime", "async error", "Send bound async".
license
MIT
compatibility
Designed for Claude Code. Requires Rust 1.85+, edition 2024.
metadata
{"author":"OpenAEC-Foundation","version":"1.0"}
rust-errors-async
Decodes the five recurring async error classes and gives one correct fix per class. Async errors are almost never bugs in / itself: they are ordinary trait, , lifetime, or runtime-policy violations that surface only because desugars into an anonymous state-machine . The fix targets the underlying violation, not the keyword.
The compiler reports future cannot be sent between threads safely or future is not Send.
A tokio::spawn call fails with a Send bound error.
The compiler reports cannot move out of a pinned value, or ... is not Unpin.
A lifetime error appears only inside an async fn body or signature.
The user asks "do I still need the async_trait crate".
The program panics at runtime with Cannot start a runtime from within a runtime.
An async program hangs, stalls, or one slow task freezes every other task.
The one rule behind every async error
An async fn is sugar for a function returning an anonymous Future. Every async error is a property of that hidden Future: is it Send, is it Unpin, what lifetimes does it capture, and is it polled by a legal runtime call.
ALWAYS identify which of those four properties failed before proposing a fix. The async keyword is never the defect.
Error class routing table
Symptom
Hidden cause
First fix to try
"future cannot be sent between threads safely"
a !Send value is live across an .await
scope the value to end before .await
"cannot move out of ... pinned" / "is not Unpin"
moving a !Unpin (self-referential) future
Box::pin, tokio::pin!, or std::pin::pin!
lifetime error inside async fn
the returned future captures every input lifetime
take owned arguments, or shorten the borrow
"async fn in trait" + dyn Trait fails
AFIT future is anonymous, unsized, not object-safe
#[async_trait] or -> Pin<Box<dyn Future>>
Cannot start a runtime from within a runtime (panic)
nested Runtime::new().block_on()
use the current runtime via Handle, or spawn_blocking
Class 1: future is not Send
error: future cannot be sent between threads safely appears when you tokio::spawn (multi-thread runtime) a future that is not Send. The spawned future must be Send + 'static.
A future is Send only if every value held live across an .await point is Send. The desugared state machine stores those values in itself, so one !Send value poisons the whole future. The error note always points at the exact line: ... which is not Send plus ... is later dropped here after an .await.
Fix A: scope the guard so it drops before .await (preferred)
// FAILS: std MutexGuard is held across .awaitasyncfnbad(data: std::sync::Arc<std::sync::Mutex<i32>>) {
letguard = data.lock().unwrap();
do_async_work().await; // guard still alive here -> future is !Sendprintln!("{}", *guard);
}
// CORRECT: read the value out, end the guard's scope, then awaitasyncfngood(data: std::sync::Arc<std::sync::Mutex<i32>>) {
letvalue = {
letguard = data.lock().unwrap();
*guard
}; // guard dropped here, nothing !Send crosses the .awaitdo_async_work().await;
println!("{}", value);
}
Fix B: use tokio::sync::Mutex when the lock MUST stay held across .await
tokio::sync::Mutex is designed so its guard is Send, and .lock() is itself async. Use it ONLY when the critical section genuinely spans an .await; for a quick non-async critical section std::sync::Mutex is faster and correct.
use tokio::sync::Mutex;
use std::sync::Arc;
asyncfnok(data: Arc<Mutex<i32>>) {
letmut guard = data.lock().await; // tokio guard IS Senddo_async_work().await; // legal: guard crosses .await
*guard += 1;
}
Fix C: convert Rc to Arc
If the !Send value is an Rc<T>, switch to Arc<T>. Do this only when sharing across threads is genuinely needed; if the value never leaves one task, prefer a current_thread runtime instead (which does not require Send).
NEVER .clone() a large structure purely to dodge this error when scoping the guard (Fix A) would work. See references/anti-patterns.md.
Class 2: cannot move out of pinned / not Unpin
async blocks compile to potentially self-referential state machines, so their Future type is !Unpin. A !Unpin future must not be moved once polling has started; Pin enforces that. Errors: cannot move out of dereference of Pin<...>, or a function requires T: Unpin but the future is not.
The fix is always to pin the future. Choose the pin location:
use std::future::Future;
// Heap pin: required when the future must outlive the current stack frame,// or be stored in a struct, Vec, or returned as Pin<Box<dyn Future>>.fnboxed(fut: implFuture<Output = ()> + 'static) -> std::pin::Pin<Box<dyn Future<Output = ()>>> {
Box::pin(fut)
}
// Stack pin: cheapest, when the future is used only within this scope.asyncfnstack_pin() {
letfut = some_async_op();
letmut fut = std::pin::pin!(fut); // std::pin::pin! since Rust 1.68poll_helper(fut.as_mut()).await;
}
// tokio::pin! is the same stack-pin tool, common in select! loops.asyncfnselect_loop() {
letsleep = tokio::time::sleep(std::time::Duration::from_secs(1));
tokio::pin!(sleep);
loop {
tokio::select! {
_ = &mut sleep => break,
_ = work() => {}
}
}
}
Decision: std::pin::pin! / tokio::pin! for stack-local use (no allocation); Box::pin when the future must be stored, returned, or type-erased. Pinning a future does NOT make it Send; that is Class 1.
NEVER wrap every future in Box::pin reflexively to silence pin errors. Most futures only need a stack pin, and select! already pins its branches.
Class 3: lifetime errors inside an async fn
An async fn returns a Future that captures every lifetime in its argument list, because the desugared state machine may need those borrows at any later poll. So a borrowed argument keeps the borrow alive for the entire duration of the returned future, not just the synchronous prologue.
// FAILS: the returned future borrows `text` for its whole lifetime,// but the caller may drop `text` while the future is still pending.asyncfnprocess(text: &str) ->usize {
do_async_work().await;
text.len()
}
// store_for_later(process(&local_string)); // borrow does not live long enough
Fix B: keep the borrow, but tie the future's lifetime explicitly
When the caller can guarantee the data outlives the future, name the lifetime:
asyncfnprocess<'a>(text: &'astr) ->usize {
do_async_work().await;
text.len()
}
// the returned `impl Future + 'a` now visibly borrows for 'a
Fix C: finish the borrow before the first .await
If the borrow is only needed synchronously, extract the owned result first:
asyncfnprocess(text: &str) ->usize {
letlen = text.len(); // borrow used and finished heredo_async_work().await;
len
}
Decision: prefer owned arguments (Fix A) for futures that are spawned or stored; keep borrows only for futures awaited immediately in the same scope. See references/methods.md for the desugaring detail.
Class 4: async fn in trait and dyn dispatch (AFIT)
Rust 1.75 stabilized async fn in traits (AFIT) and -> impl Trait in traits (RPITIT). For static dispatch (generic T: MyTrait or impl MyTrait), native async fn in a trait works with no crate:
// WORKS on stable since 1.75 for static dispatchtraitFetch {
asyncfnget(&self, url: &str) ->String;
}
structClient;
implFetchforClient {
asyncfnget(&self, url: &str) ->String {
format!("body of {url}")
}
}
// static dispatch: monomorphized, no boxingasyncfnuse_static<F: Fetch>(f: &F) ->String { f.get("x").await }
The limitation: a native AFIT method returns an anonymous, unsized future, so the trait is not object-safe through that method. dyn Fetch does NOT compile, and there is no Send bound you can write on the AFIT return future at the trait level.
#[async_trait] crate, OR manual -> Pin<Box<dyn Future + Send>>
a Send bound on the returned future for spawning
#[async_trait], OR the trait-variant crate's #[trait_variant::make(... : Send)]
// dyn dispatch path: async_trait boxes the future so the trait is object-safe#[async_trait::async_trait]traitFetch {
asyncfnget(&self, url: &str) ->String;
}
// now `Box<dyn Fetch>` and `&dyn Fetch` compile// Manual equivalent without the crate:traitFetchManual {
fnget<'a>(&'aself, url: &'astr)
-> std::pin::Pin<Box<dyn std::future::Future<Output = String> + Send + 'a>>;
}
Decision: use native AFIT by default. Reach for #[async_trait] ONLY when you genuinely need dyn Trait or a trait-level Send bound. NEVER add #[async_trait] to a trait that is only ever used with generic static dispatch; it forces a heap allocation per call for no reason.
Class 5: blocking the runtime and nested runtimes
These are runtime-policy errors. Some panic; some only manifest as hangs.
Cannot start a runtime from within a runtime (panic)
Calling tokio::runtime::Runtime::new().unwrap().block_on(...), or futures::executor::block_on, from inside code already driven by a Tokio runtime panics or deadlocks. You already have a runtime; do not create a second.
// PANICS: creates a nested runtime inside #[tokio::main]#[tokio::main]asyncfnmain() {
letrt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(work()); // "Cannot start a runtime from within a runtime"
}
// CORRECT: you are already async, just .await#[tokio::main]asyncfnmain() {
work().await;
}
// From a SYNC function called inside async code, reuse the current runtime:fnsync_bridge() {
lethandle = tokio::runtime::Handle::current();
letresult = tokio::task::block_in_place(|| handle.block_on(work()));
let_ = result;
}
Blocking calls starve the executor (hang, no panic)
std::thread::sleep, synchronous file/network I/O, std::sync::Mutex contention, and CPU-bound loops block the worker thread. The runtime cannot detect this; the symptom is that unrelated tasks stop making progress.
// BAD: blocks the worker thread, freezes other tasksasyncfnbad() {
std::thread::sleep(std::time::Duration::from_secs(5)); // blocks the executorexpensive_cpu_loop(); // also blocks
}
// CORRECT: async sleep yields; CPU work goes to the blocking poolasyncfngood() {
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
letresult = tokio::task::spawn_blocking(|| expensive_cpu_loop()).await.unwrap();
let_ = result;
}
Decision: tokio::time::sleep and tokio::fs for async-native equivalents; spawn_blocking for synchronous third-party I/O and CPU-bound work; block_in_place ONLY when the blocking work must stay on the current task and the runtime is multi-thread (it panics on a current_thread runtime). NEVER run a CPU-bound loop directly on an async worker.
Fix-selection decision tree
Async error?
├─ "future cannot be sent between threads safely" (Class 1)
│ ├─ value only needed before .await -> scope it in a { } block
│ ├─ lock MUST span the .await -> tokio::sync::Mutex
│ └─ value is an Rc shared cross-task -> Arc, or current_thread runtime
├─ "cannot move out of pinned" / "not Unpin" (Class 2)
│ ├─ future used only in this scope -> std::pin::pin! / tokio::pin!
│ └─ future stored / returned / erased -> Box::pin
├─ lifetime error inside async fn (Class 3)
│ ├─ future is spawned or stored -> take owned arguments
│ └─ future awaited immediately -> name 'a, or finish borrow pre-.await
├─ async fn in trait fails (Class 4)
│ ├─ generic / impl Trait dispatch -> native AFIT, no crate
│ └─ dyn Trait or trait-level Send -> #[async_trait] or Pin<Box<dyn Future>>
└─ runtime panic / hang (Class 5)
├─ "runtime from within a runtime" -> .await directly, or Handle::current()
└─ tasks freeze, no panic -> spawn_blocking / tokio::time::sleep
Reference files
references/methods.md: the Future trait and poll contract, Pin/Unpin invariants, why async fn captures all input lifetimes, the Send + 'static spawn bound, tokio::sync::Mutex vs std::sync::Mutex, AFIT vs RPITIT object-safety rules, block_in_place / spawn_blocking / Handle API surface.
references/examples.md: full failing-then-fixed programs for every class, including the MutexGuard-across-await note output, the select! pin loop, the AFIT static-vs-dyn split, and the nested-runtime panic with its fix.
references/anti-patterns.md: holding std::sync::MutexGuard across .await, reflexive Box::pin, #[async_trait] on static-only traits, block_on inside async context, CPU-bound work on async workers, cloning huge data to dodge a Send error.