Threads

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

  1. Holding a MutexGuard across I/O or thread::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.
  2. Spawning unbounded OS threads per network connection: Use a bounded ThreadPool or an async executor (tokio) when connection counts can spike into the thousands.
  3. 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.