| name | rust-errors-async |
| description | 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 async/.await itself: they are ordinary trait, Send, lifetime, or runtime-policy violations that surface only because async fn desugars into an anonymous state-machine Future. The fix targets the underlying violation, not the async keyword.
Cross-references: [[rust-syntax-async-await]] (async/await mechanics, desugaring), [[rust-impl-async-tokio]] (Tokio runtime setup, spawn, channels), [[rust-core-async-runtime]] (Future/Pin/Waker model), [[rust-agents-compile-fix]] (automated compile-fix loop).
When to use this skill
- 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.
Common !Send offenders held across .await:
std::sync::MutexGuard / RwLockReadGuard / RwLockWriteGuard
Rc<T>, rc::Weak<T>
RefCell borrow guards (Ref<T>, RefMut<T>)
- raw pointers
*const T / *mut T
Fix A: scope the guard so it drops before .await (preferred)
async fn bad(data: std::sync::Arc<std::sync::Mutex<i32>>) {
let guard = data.lock().unwrap();
do_async_work().await;
println!("{}", *guard);
}
async fn good(data: std::sync::Arc<std::sync::Mutex<i32>>) {
let value = {
let guard = data.lock().unwrap();
*guard
};
do_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;
async fn ok(data: Arc<Mutex<i32>>) {
let mut guard = data.lock().await;
do_async_work().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;
fn boxed(fut: impl Future<Output = ()> + 'static) -> std::pin::Pin<Box<dyn Future<Output = ()>>> {
Box::pin(fut)
}
async fn stack_pin() {
let fut = some_async_op();
let mut fut = std::pin::pin!(fut);
poll_helper(fut.as_mut()).await;
}
async fn select_loop() {
let sleep = 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.
async fn process(text: &str) -> usize {
do_async_work().await;
text.len()
}
Fix A: take an owned argument
async fn process(text: String) -> usize {
do_async_work().await;
text.len()
}
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:
async fn process<'a>(text: &'a str) -> usize {
do_async_work().await;
text.len()
}
Fix C: finish the borrow before the first .await
If the borrow is only needed synchronously, extract the owned result first:
async fn process(text: &str) -> usize {
let len = text.len();
do_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:
trait Fetch {
async fn get(&self, url: &str) -> String;
}
struct Client;
impl Fetch for Client {
async fn get(&self, url: &str) -> String {
format!("body of {url}")
}
}
async fn use_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.
When async_trait is still required
| Need | Use |
|---|
static dispatch, generic bound or impl Trait | native async fn in trait, no crate |
dyn Trait object (trait objects, heterogeneous collections) | #[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)] |
#[async_trait::async_trait]
trait Fetch {
async fn get(&self, url: &str) -> String;
}
trait FetchManual {
fn get<'a>(&'a self, url: &'a str)
-> 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.
#[tokio::main]
async fn main() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(work());
}
#[tokio::main]
async fn main() {
work().await;
}
fn sync_bridge() {
let handle = tokio::runtime::Handle::current();
let result = 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.
async fn bad() {
std::thread::sleep(std::time::Duration::from_secs(5));
expensive_cpu_loop();
}
async fn good() {
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
let result = 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.
Sources