The errors that block every new Tokio codebase, how to read them, and the fix for each. The compiler output below is abridged; the key lines are verbatim.
Contents
- Why spawn needs Send + 'static
- Reading "future cannot be sent between threads safely"
- MutexGuard held across await
- Rc and RefCell in async code
- Other !Send values: dyn Error, raw pointers, ThreadRng
- E0373: async block may outlive the current function
- E0597 and borrowed for 'static
- axum: Handler trait is not satisfied
- Router state type mismatches
- async fn in traits
- Recursion and boxing
- Asserting Send in tests
- Runtime panics that look like type errors
Why spawn needs Send + 'static
pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static;'static: the task may outlive the function that spawned it, so it cannot borrow the caller's locals. Own the data (move, clone,Arc).Send: on the multi-thread runtime the task can resume on another worker after any.await. Every value alive across an.awaitis stored in the future's state machine, so all of them must beSend. Values created and dropped between two awaits do not matter.
Reading "future cannot be sent between threads safely"
error: future cannot be sent between threads safely
--> src/main.rs:20:18
|
20 | tokio::spawn(update(state));
| ^^^^^^^^^^^^^ future returned by `update` is not `Send`
|
= help: within `impl Future<Output = ()>`, the trait `Send` is not implemented for `std::sync::MutexGuard<'_, Vec<u64>>`
note: future is not `Send` as this value is used across an await
--> src/main.rs:12:28
|
11 | let mut guard = state.lock().unwrap();
| --------- has type `std::sync::MutexGuard<'_, Vec<u64>>` which is not `Send`
12 | tick().await;
| ^^^^^ await occurs here, with `mut guard` maybe used later
note: required by a bound in `tokio::spawn`Read it bottom-up: the note names which value (guard), its type (MutexGuard), and which
await it crosses. The fix is always one of:
- End the value's lifetime before the
.await(inner block,drop(x), or extract a clone). - Replace it with a
Sendalternative (ArcforRc,tokio::sync::Mutexguard,Box<dyn Error + Send + Sync>). - Run the future on a
LocalSet(spawn_local) when it truly must be!Send.
Note that drop(guard) before the await works on current compilers; older compilers sometimes still counted
the variable as live. If drop does not help, use an inner block.
MutexGuard held across await
use std::sync::{Arc, Mutex};
// Does not compile when spawned: guard lives across .await
async fn bad(state: Arc<Mutex<Vec<u64>>>) {
let mut v = state.lock().unwrap();
v.push(1);
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
v.push(2);
}
// Fix 1: scope the guard
async fn good_scoped(state: Arc<Mutex<Vec<u64>>>) {
{
let mut v = state.lock().unwrap();
v.push(1);
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
state.lock().unwrap().push(2);
}
// Fix 2: move the locking into a sync helper
fn push(state: &Mutex<Vec<u64>>, x: u64) {
state.lock().unwrap().push(x);
}
// Fix 3: the guard really must span the await -> tokio::sync::Mutex
async fn good_async_mutex(state: Arc<tokio::sync::Mutex<Vec<u64>>>) {
let mut v = state.lock().await;
v.push(1);
tokio::time::sleep(std::time::Duration::from_millis(10)).await; // other lockers wait, by design
v.push(2);
}Same for RwLockReadGuard/RwLockWriteGuard, parking_lot guards, dashmap::mapref::one::Ref, and
tokio::sync::watch::Ref (from borrow()). Clippy has lints for this: clippy::await_holding_lock and
clippy::await_holding_refcell_ref (both warn by default in the suspicious group).
If it compiles because you are on current_thread or a LocalSet, it is still wrong: a std mutex held while
the task is suspended blocks any other task that tries to lock it, and on a single thread that is a deadlock.
Rc and RefCell in async code
error: future cannot be sent between threads safely
= help: within `impl Future<Output = ()>`, the trait `Send` is not implemented for `Rc<RefCell<Vec<String>>>`
note: future is not `Send` as this value is used across an awaitAlso seen as `Rc<...>` cannot be sent between threads safely when an Rc is moved into a spawned
block.
use std::sync::{Arc, Mutex};
async fn collect(urls: Vec<String>) -> Vec<String> {
let seen = Arc::new(Mutex::new(Vec::new())); // was Rc<RefCell<Vec<_>>>
let mut set = tokio::task::JoinSet::new();
for url in urls {
let seen = Arc::clone(&seen);
set.spawn(async move {
tokio::task::yield_now().await;
seen.lock().unwrap().push(url);
});
}
while set.join_next().await.is_some() {}
let out = seen.lock().unwrap().clone();
out
}Rc is fine in sync helper functions called from async code as long as it does not live across an
.await. Cell/RefCell are Send (if the content is) but not Sync, so &RefCell across an await or an
Arc<RefCell<_>> fails similarly; use Mutex or atomics.
Other !Send values: dyn Error, raw pointers, ThreadRng
Value alive across .await | Fix |
|---|---|
Box<dyn std::error::Error> | Box<dyn Error + Send + Sync> or anyhow::Error |
rand::rngs::ThreadRng (rand::thread_rng()/rand::rng()) | generate the number before the await, or keep a Send RNG such as rand::rngs::StdRng (seeded once) |
*const T / *mut T | wrap in a type with a justified unsafe impl Send, or keep it out of async code |
std::sync::MutexGuard, RwLock*Guard, Ref/RefMut | scope it, see above |
tracing::span::Entered (span.enter()) | never enter() across await; use .instrument(span) or #[instrument] |
&T where T: !Sync | T: Sync (Mutex) or own a clone |
| non-Send SDK client | LocalSet + spawn_local, or a dedicated thread/actor that owns it |
use tracing::Instrument;
async fn traced(id: u64) {
let span = tracing::info_span!("job", id);
async move {
tokio::task::yield_now().await;
tracing::info!("inside span across awaits");
}
.instrument(span) // correct; `let _e = span.enter();` across .await is wrong
.await;
}E0373: async block may outlive the current function
error[E0373]: async block may outlive the current function, but it borrows `name`, which is owned by the current function
--> src/main.rs:6:18
|
6 | tokio::spawn(async {
| ^^^^^ may outlive borrowed value `name`
7 | println!("{name}");
| ---- `name` is borrowed here
help: to force the async block to take ownership of `name` (and any other referenced variables), use the `move` keywordasync fn start(name: String, state: std::sync::Arc<AppState>) {
let task_state = state.clone(); // clone what the task needs
tokio::spawn(async move { // move ownership into the task
task_state.log(&name).await;
});
state.log("parent still has its Arc").await;
}Pattern for loops: clone inside the loop, right before the async move.
E0597 and borrowed for 'static
error[E0597]: `config` does not live long enough
...
argument requires that `config` is borrowed for `'static`Triggered by passing &local into tokio::spawn, JoinSet::spawn, std::thread::spawn, or storing a
reference in a 'static struct. Fixes:
- Put shared data in an
Arcand clone theArc. - For read-only config that lives for the whole program,
Box::leak(Box::new(cfg))yields&'static Config(acceptable once at startup, never per request). - For structured fan-out that genuinely borrows, do not spawn: run the futures concurrently on the current task
with
futures::future::join_allorFuturesUnordered, which have no'staticbound.
use futures::{StreamExt, stream::FuturesUnordered};
async fn check_all(urls: &[String], http: &reqwest::Client) -> usize {
let mut pending: FuturesUnordered<_> = urls.iter().map(|u| http.head(u).send()).collect();
let mut ok = 0;
while let Some(res) = pending.next().await {
if res.is_ok_and(|r| r.status().is_success()) {
ok += 1;
}
}
ok
}Concurrent but not parallel: everything runs inside the calling task. Bound it (buffer_unordered(n)) for
large inputs.
axum: Handler trait is not satisfied
error[E0277]: the trait bound `fn(Json<NewOrder>, State<Arc<AppState>>) -> impl Future<Output = ...> {create}: Handler<_, _>` is not satisfiedThe message never says why. Causes, in order of frequency:
- A body extractor (
Json,Form,String,Bytes,Request) is not the last argument. - The handler's future is not
Send(a guard/Rcacross an await inside it). - An argument does not implement
FromRequestParts/FromRequest(a plainu64instead ofPath<u64>, a custom type without an extractor impl). - The return type does not implement
IntoResponse(e.g.Result<T, anyhow::Error>: implementIntoResponsefor your own error type). State<T>whereTdiffers from the router's state type (and noFromRef).- More than 16 arguments.
Add #[axum::debug_handler] (feature macros) above the handler: it produces a targeted error such as
"Json<_> consumes the request body and thus must be the last argument to the handler function" or "future
cannot be sent between threads safely". Remove it afterwards or keep it; it has no runtime cost.
Router state type mismatches
| Error | Cause | Fix |
|---|---|---|
expected Router, found Router<Arc<AppState>> at axum::serve | with_state not called | .with_state(state) last |
the trait bound ... Handler<_, Arc<AppState>> for a sub-router | nested router has a different state type | give both the same S, or with_state on the nested router first |
FromRef<AppState> not implemented for PgPool | handler extracts a substate | #[derive(FromRef)] on the state struct (needs Clone) |
async fn in traits
Stable since Rust 1.75:
pub trait OrderRepo {
async fn find(&self, id: i64) -> anyhow::Result<Option<Order>>;
}Limitations:
- Not dyn-compatible:
Box<dyn OrderRepo>/Arc<dyn OrderRepo>does not work. Use generics (AppState<R: OrderRepo>), an enum of implementations, or theasync_traitcrate (boxes every future), or write the method as returningPin<Box<dyn Future<Output = _> + Send + '_>>yourself. - No
Sendguarantee for callers: generic code that spawnsrepo.find(1)cannot prove the future isSend. rustc warns (async_fn_in_traitlint) on public traits. Declare the bound explicitly:
use std::future::Future;
pub trait OrderRepo: Send + Sync + 'static {
fn find(&self, id: i64) -> impl Future<Output = anyhow::Result<Option<Order>>> + Send;
}
pub struct PgRepo { pool: sqlx::PgPool }
impl OrderRepo for PgRepo {
async fn find(&self, id: i64) -> anyhow::Result<Option<Order>> { // async fn satisfies the impl Future signature
Ok(sqlx::query_as::<_, Order>("SELECT id, sku, qty FROM orders WHERE id = $1")
.bind(id)
.fetch_optional(&self.pool)
.await?)
}
}The trait-variant crate (#[trait_variant::make(SendOrderRepo: Send)]) generates a Send variant from an
async fn trait. For Arc<dyn Repo> in axum state, async_trait is still the practical choice.
Recursion and boxing
error[E0733]: recursion in an async fn requires boxinguse std::{future::Future, pin::Pin};
fn walk(depth: u32) -> Pin<Box<dyn Future<Output = u32> + Send>> {
Box::pin(async move {
if depth == 0 { 0 } else { 1 + walk(depth - 1).await }
})
}Since Rust 1.77 an async fn may recurse when the recursive call is boxed: Box::pin(walk(depth - 1)).await.
Asserting Send in tests
Catch !Send regressions where they are introduced, not three layers up at tokio::spawn:
#[allow(dead_code)]
fn assert_send<T: Send>(_: &T) {}
#[test]
fn handler_futures_are_send() {
let state = test_state();
let fut = load_order_for_test(state, 1); // do not await; just build the future
assert_send(&fut);
}Runtime panics that look like type errors
| Panic message | Cause | Fix |
|---|---|---|
Cannot start a runtime from within a runtime. This happens because a function (like block_on) attempted to block the current thread while the thread is being used to drive asynchronous tasks. | Runtime::new().block_on, Handle::block_on, futures::executor::block_on of tokio I/O inside async code | .await; or call from spawn_blocking |
Cannot drop a runtime in a context where blocking is not allowed | reqwest::blocking::Client (owns a runtime) created/dropped in async code | use the async client |
can call blocking only when running on the multi-threaded runtime | block_in_place on current_thread / #[tokio::test] | spawn_blocking, or flavor = "multi_thread" |
there is no reactor running, must be called from the context of a Tokio 1.x runtime | tokio API used on a non-runtime thread, or with another executor | pass a Handle, handle.spawn; or a runtime started with enable_all() |
A Tokio 1.x context was found, but timers are disabled | builder without enable_time() | enable_all() |
Path segments must not start with ':' | axum 0.7 path syntax on 0.8 | /{id} |
Overlapping method route | same method registered twice for a path | remove duplicate |
rust-borrow-checker-error-explainer
Explain and fix Rust borrow-checker, ownership, move and lifetime compiler errors: E0382 use of moved value, E0499 cannot borrow as mutable more than once, E0502 also borrowed as immutable, E0505, E0506, E0503, E0507 cannot move out of index/Rc/reference, E0515 return reference to local, E0597 does not live long enough, E0716 temporary value dropped while borrowed, E0373 closure may outlive (spawn needs move), E0521 borrowed data escapes, E0384, E0596, E0594, E0381, E0713, 'lifetime may not live long enough', RefCell 'already borrowed: BorrowMutError' panics. Ranks fixes from restructuring (split borrows, entry API, split_at_mut, get_disjoint_mut, mem::take, Option::take, indices, thread::scope) down to clone and Rc<RefCell>/Arc<Mutex>; never unsafe. Covers NLL limits (get-or-insert), iterator invalidation, closure captures, self-referential structs. Ships std-only Rust triage tools.
rust-cargo-workspace-designer
Design, review and fix Cargo workspaces: when to split a crate (compile time, boundaries, publishing, proc-macros), crates/* vs apps/ + crates/ layouts, virtual manifest vs root package, members/exclude/default-members, resolver 2 vs 3 (MSRV-aware, edition 2024), [workspace.package] and [workspace.dependencies] inheritance, [workspace.lints], root-only profiles (lto, codegen-units), additive features with dep: and weak ?/ syntax, feature unification pitfalls, internal path + version deps, publish = false, Cargo.lock and MSRV policy, xtask, CI with --locked, nextest, rust-cache, sccache, cargo-hack/deny/machete/udeps/semver-checks, and cargo build --timings. Ships a workspace auditor and crate-graph tool.
rust-error-handling-thiserror-anyhow-designer
Design and review Rust error handling with thiserror 2 and anyhow 1: library vs application split, typed error enums with #[from], #[source], #[error(transparent)] and #[non_exhaustive], ErrorKind patterns, context via .context()/.with_context(), bail!/ensure!, source() chains, {:#} and {:?} reports, RUST_BACKTRACE/RUST_LIB_BACKTRACE, Box<dyn Error + Send + Sync>, main -> anyhow::Result or ExitCode, unwrap/expect/panic policy, Option to Result, collecting iterators of Results, axum IntoResponse mapping, retryable errors, tracing, async Send bounds, testing with matches! and downcast_ref, no_std core::error::Error, and snafu, eyre/color-eyre, miette, error-stack.
rust-lifetime-annotation-advisor
Decide when Rust lifetime annotations are needed and which ones: the three elision rules, structs holding references (struct Parser<'a>), owned vs borrowed (String, &str, Cow<'a, str>, Arc<str>), one vs several lifetimes, 'a: 'b bounds, T: 'static misconceptions, Box<dyn Trait + 'a> default object lifetimes, impl Trait + '_ and edition 2024 capture rules with use<..>, variance and PhantomData, HRTB for<'a> Fn(&'a T), GATs and lending iterators, async fn and tokio::spawn 'static, self-referential structs (ouroboros, self_cell, indices, arenas), Rc/Arc to escape lifetime soup. Ships std-only Rust checkers for redundant, missing, unused and over-restrictive lifetimes.
rust-trait-design-reviewer
Review and design Rust traits and generic APIs: generics vs impl Trait vs dyn Trait (static vs dynamic dispatch), dyn compatibility / object safety (generic methods, Self returns, where Self: Sized, error[E0038] not dyn compatible / cannot be made into an object), associated types vs generic parameters, blanket impls, coherence and orphan rule (E0117, E0119, E0210), newtype, extension, sealed and marker traits, supertraits and trait upcasting (1.86), async fn in traits (1.75) and the Send bound problem (trait_variant, async-trait), RPITIT, GATs, From/Into/TryFrom/AsRef/Borrow, std trait checklist (Debug, Clone, PartialEq, Hash, Default, Display, Error, Send/Sync, serde feature), #[must_use], #[non_exhaustive], semver hazards, Deref abuse, builders, typestate, PhantomData, Box<dyn Error + Send + Sync>, Fn traits, mockall.
rust-unsafe-block-auditor
Audit unsafe Rust for soundness: inventory every unsafe block, unsafe fn, unsafe impl/trait, extern block and unsafe attribute, check // SAFETY: comments and # Safety docs, and classify each as sound, needs-justification or likely UB, proposing safe alternatives first. Covers the five unsafe superpowers, aliasing (Stacked/Tree Borrows), invalid values, MaybeUninit vs mem::zeroed/uninitialized, transmute, slice::from_raw_parts, Vec::set_len/from_raw_parts, get_unchecked, CString::as_ptr dangling, unsafe impl Send/Sync, Pin projection, FFI (extern "C", repr(C), C-unwind, Box::into_raw/from_raw), strict provenance, static mut and &raw, edition 2024 (unsafe extern, #[unsafe(no_mangle)], unsafe_op_in_unsafe_fn, env::set_var), Miri, sanitizers, cargo-fuzz, loom, cargo-geiger and clippy safety lints.