Skip to content
v0.1.1latest release34 skills2 categories4 sub-categories79 tested scripts157 reference docsContact us

Threads, Async and Closures

Reference for rust-borrow-checker-error-explainer · 244 lines

Programming / Rustv1.0.0Updated OCT 21 20267 references5 scripts

Borrow errors around thread::spawn, tokio::spawn and closures all come from one rule: work that may outlive the current function must own everything it touches ('static), and futures sent to a multi-threaded runtime must be Send at every .await.

Contents

Why spawn needs 'static

rust
// std
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where F: FnOnce() -> T + Send + 'static, T: Send + 'static;
// tokio
pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
where F: Future + Send + 'static, F::Output: Send + 'static;

The spawned thread/task may still run after the caller returns, so it cannot hold references to the caller's locals. 'static here means "contains no non-'static borrows", not "lives forever": an owned String, Vec, Arc<T> or Sender is 'static.

E0373: add move, then own the data

Without move, a closure captures variables by reference. The compiler's help suggests move, which is correct when the captured variables are owned values:

rust
use std::thread;

fn report(lines: Vec<String>) -> usize {
    // let h = thread::spawn(|| lines.len());   // E0373: borrows `lines`
    let h = thread::spawn(move || lines.len()); // the Vec is moved into the thread
    h.join().unwrap()
}

If the caller still needs the value afterwards, clone it (or an Arc around it) before the closure and move the clone in:

rust
use std::sync::Arc;
use std::thread;

fn fan_out(data: Arc<Vec<u64>>) -> u64 {
    let workers: Vec<_> = (0..4)
        .map(|i| {
            let data = Arc::clone(&data);           // shadowed clone, moved below
            thread::spawn(move || data.iter().skip(i).step_by(4).sum::<u64>())
        })
        .collect();
    let total = workers.into_iter().map(|h| h.join().unwrap()).sum();
    println!("still usable here: {} items", data.len());
    total
}

E0597 / E0521: move is not enough when you move a reference

If what you move is itself a reference (&state, a &self field, a &str parameter), the closure is still borrowing, and the error becomes E0597 "state does not live long enough ... argument requires that state is borrowed for 'static", or E0521 "borrowed data escapes outside of method".

rust
use std::sync::Arc;
use std::thread;

struct Service { name: String }

impl Service {
    // fn start(&self) { thread::spawn(move || println!("{}", self.name)); }   // E0521: `self` is &Service

    fn start(self: &Arc<Self>) -> thread::JoinHandle<()> {
        let me = Arc::clone(self);                 // own a handle
        thread::spawn(move || println!("{} started", me.name))
    }

    fn start_with_name(&self) -> thread::JoinHandle<()> {
        let name = self.name.clone();              // or own just the field you need
        thread::spawn(move || println!("{name} started"))
    }
}

Fixes, best first: move owned data (clone the small field), take Arc<Self>/Arc<T> parameters, or use a scoped thread if the work finishes before the function returns.

Scoped threads borrow safely

std::thread::scope (1.63) guarantees every thread spawned in the scope is joined before scope returns, so the threads may borrow locals, including mutably and disjointly.

rust
use std::thread;

fn parallel_sum_and_scale(values: &mut [f64], factor: f64) -> f64 {
    let (left, right) = values.split_at_mut(values.len() / 2);
    thread::scope(|s| {
        s.spawn(|| left.iter_mut().for_each(|x| *x *= factor));   // &mut left
        s.spawn(|| right.iter_mut().for_each(|x| *x *= factor));  // &mut right, disjoint
    });                                                            // both joined here
    values.iter().sum()
}

Use move inside the scope only to move copies of small locals (s.spawn(move || ...) copies & slices, which is still borrowing the outer data). Rayon's par_iter and rayon::scope follow the same model.

Sharing results back

NeedTool
One result per threadreturn it from the closure and join()
Stream results while runningstd::sync::mpsc::channel / tokio::sync::mpsc (clone the Sender per worker)
Accumulate into one valueArc<Mutex<T>>, or atomics for counters
Borrow inputs, return outputs, all before the function endsthread::scope with ScopedJoinHandle::join
rust
use std::sync::mpsc;
use std::thread;

fn crawl(urls: Vec<String>) -> Vec<(String, usize)> {
    let (tx, rx) = mpsc::channel();
    for url in urls {
        let tx = tx.clone();
        thread::spawn(move || {
            let len = url.len(); // stand-in for a fetch
            tx.send((url, len)).ok();
        });
    }
    drop(tx); // close the original sender so `rx` ends when workers finish
    rx.into_iter().collect()
}

Tokio: spawn, Send and guards across .await

Same 'static rule: tokio::spawn(async move { ... }) with owned or Arc captures. The extra rule is Send: on the multi-threaded runtime a task may move between threads at each .await, so every value alive across an .await must be Send.

rust
use std::sync::{Arc, Mutex};

async fn record(state: Arc<Mutex<Vec<u32>>>, v: u32) {
    // let mut g = state.lock().unwrap();
    // g.push(v);
    // flush().await;            // error: future cannot be sent between threads safely
    //                           // (`MutexGuard` is !Send and is still alive here)
    {
        let mut g = state.lock().unwrap();
        g.push(v);
    }                            // guard dropped before the await
    flush().await;
}

async fn flush() {}
Value alive at .awaitProblemFix
std::sync::MutexGuard!Sendscope it in a block or drop(guard) before .await
Rc<T>, RefCell Ref/RefMut!SendArc, or keep them out of the async fn's live state
A guard that truly must be held across .awaitstd guard is wrong heretokio::sync::Mutex (async lock().await)
A !Send future you cannot changemulti-threaded runtimetokio::task::spawn_local on a LocalSet, or current_thread runtime

Older compilers did not see an explicit drop(guard) when checking Send; a block scope works on every version, so prefer it. parse_diagnostics reports these as BCK026 (not-send).

Closures: Fn, FnMut, FnOnce and captures

A closure captures each variable in the least powerful way its body needs:

Body doesCaptured asClosure implements
reads x&xFn (+ FnMut + FnOnce)
mutates x&mut xFnMut (+ FnOnce)
moves x out (drops it, returns it, passes by value)x by valueFnOnce only
move keywordeverything by value (copies for Copy types)depends on the body as above

Edition 2021 captures disjoint fields: || self.count += 1 captures self.count, not all of self.

Common errors:

rust
fn counters() {
    let mut n = 0;
    let mut inc = || n += 1;      // FnMut: holds &mut n while `inc` is alive
    inc();
    inc();
    // println!("{n}"); inc();    // E0502: `n` read while `inc` (and its &mut n) is still used
    println!("{n}");              // fine: `inc` is not used after this point
}

fn call_twice<F: FnMut()>(mut f: F) { f(); f(); }

fn names() -> Vec<String> {
    let mut out = Vec::new();
    call_twice(|| out.push("x".to_string())); // FnMut is enough; `out` is usable again afterwards
    out
}

"cannot move out of x, a captured variable in an FnMut closure" (E0507) means a closure that may be called again tries to give away a captured value: clone inside the closure, or use Option::take.

Returning closures

rust
fn make_prefixer(prefix: String) -> impl Fn(&str) -> String {
    move |s| format!("{prefix}{s}")            // owns `prefix`
}

fn make_matcher<'a>(words: &'a [&'a str]) -> impl Fn(&str) -> bool + 'a {
    move |s| words.iter().any(|w| *w == s)     // borrows `words` for 'a: lifetime in the return type
}

Returning a closure that borrows a local fails with E0373/E0597; move the data in, or tie the closure's lifetime to an input as in make_matcher. In edition 2024, impl Trait in return position captures all in-scope lifetimes automatically; use + use<'a> (1.82) to state precisely which ones.

Quick reference

ErrorContextFix
E0373`thread::spawn(
E0597 "borrowed for 'static"moved a &local into a spawned taskown the data, Arc, or thread::scope
E0521&self/&T parameter used in a spawned taskclone the field or take Arc<Self>
future cannot be sentMutexGuard/Rc alive at .awaitscope the guard; Arc; tokio::sync::Mutex if needed
E0502 with a closureFnMut closure still alive while the variable is usedfinish using the closure first; pass as argument
E0507 in FnMutclosure gives away a captured valueclone inside, Option::take, or make it FnOnce
More in Rust

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-tokio-async-service-builder

Build and review production async Rust services on Tokio 1.x and axum 0.8: runtime choice (#[tokio::main] multi_thread vs current_thread, Builder), axum routing with /{id} paths, State, extractors, IntoResponse error types, axum::serve with_graceful_shutdown, tower/tower-http middleware (TraceLayer, TimeoutLayer, CorsLayer, ConcurrencyLimitLayer, RequestBodyLimitLayer), Arc<AppState>, std vs tokio Mutex, JoinSet, TaskTracker, CancellationToken, select! cancel safety, bounded mpsc, oneshot, broadcast, watch, actors, Semaphore, timeouts, retries, spawn_blocking, sqlx PgPool, reqwest Client reuse, tracing, tests with start_paused and oneshot, Dockerfile. Fixes 'future cannot be sent between threads safely', E0373, Rc or MutexGuard held across await, 'Cannot start a runtime from within a runtime', 'Path segments must not start with :'. Ships std-only Rust scanners and a compiling axum 0.8 scaffold.

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.