Common Threading Errors
Because Rust rejects thread-unsafe code at compile time and enforces strict RAII lock scopes at runtime, threading errors in Rust fall into two clear categories: compile-time borrow/trait errors (E0373, E0382, E0277) and runtime concurrency anti-patterns (serialized .join() loops, overly broad MutexGuard scopes, and silent detached panics).
Summary of Compile-Time Threading Errors and Their Fixes
| Compiler Error | Typical Cause | Idiomatic Fix |
|---|---|---|
E0373: closure may outlive the current function, but it borrows x |
Omitting move on a thread::spawn closure, or trying to borrow a stack variable across thread::spawn |
Add move to transfer ownership, or switch from thread::spawn to std::thread::scope to borrow the stack |
E0382: use of moved value x in loop |
Moving an Arc<T>, Sender<T>, or String into thread::spawn(move || ...) inside a for loop |
Clone the handle (let x_clone = Arc::clone(&x);) at the top of the loop body before move || |
E0277: Rc<T> cannot be sent between threads safely |
Using single-threaded Rc<T> instead of Arc<T> |
Replace std::rc::Rc<T> with std::sync::Arc<T> |
E0277: RefCell<T> cannot be shared between threads safely |
Wrapping RefCell<T> or Cell<T> inside Arc<...> |
Replace Arc<RefCell<T>> with Arc<Mutex<T>>, Arc<RwLock<T>>, or Arc<Atomic*> |
E0597: x does not live long enough (in thread::scope) |
Declaring let x inside thread::scope(|s| { let x = ...; s.spawn(|| &x); }) instead of outside thread::scope |
Declare let x before `thread::scope( |
Subtle Error: Declaring Borrowed Data Inside thread::scope Instead of Outside (E0597)
A common surprise with std::thread::scope is declaring a local variable inside the thread::scope(|s| { ... }) closure and trying to borrow it inside s.spawn(...). Because the thread::scope closure returns before thread::scope joins its spawned threads, variables declared inside the closure drop before the threads finish!
// ❌ does not compile
use std::thread;
fn main() {
thread::scope(|s| {
// Mistake: `inner_vec` is declared INSIDE the `|s|` closure, so it drops
// when the closure returns — BEFORE `thread::scope` joins `s.spawn`!
let inner_vec = vec![1, 2, 3];
s.spawn(|| {
println!("{:?}", inner_vec);
});
});
}
error[E0597]: `inner_vec` does not live long enough
--> src/main.rs:9:30
|
4 | thread::scope(|s| {
| - has type `&'1 Scope<'1, '_>`
...
7 | let inner_vec = vec![1, 2, 3];
| --------- binding `inner_vec` declared here
8 | s.spawn(|| {
| - -- value captured here
| |
| argument requires that `inner_vec` is borrowed for `'1`
9 | println!("{:?}", inner_vec);
| ^^^^^^^^^ borrowed value does not live long enough
10 | });
11 | });
| - `inner_vec` dropped here while still borrowed
Moving let inner_vec = vec![1, 2, 3]; above thread::scope(|s| ...) (or explicitly calling .join() on the ScopedJoinHandle before inner_vec goes out of scope) fixes the lifetime immediately:
use std::thread;
fn main() {
// Declared BEFORE `thread::scope` -> has lifetime `'env`, which outlives `'scope`:
let outer_vec = vec![1, 2, 3];
thread::scope(|s| {
s.spawn(|| {
println!("borrowed outer_vec safely: {:?}", outer_vec);
});
});
}
borrowed outer_vec safely: [1, 2, 3]
Runtime Threading Anti-Patterns Checklist
- Holding a
MutexGuardacross I/O orthread::sleep: Locks out all other threads for milliseconds instead of nanoseconds; copy or extract the needed data inside a{ ... }block so the guard drops before sleeping or doing I/O. - Spawning unbounded OS threads per network connection: Use a bounded
ThreadPoolor anasyncexecutor (tokio) when connection counts can spike into the thousands. - Ignoring
JoinHandles when panic detection matters: Detached threads that panic do not notify the main thread unless joined or monitored via a channel or panic hook.