Rust Error E0373: Closure May Outlive the Current Function
E0373 means a closure borrows a local variable, but the closure might keep running (or keep existing) after the function that owns that variable has finished. You usually meet it the first time you call thread::spawn or return a closure from a function. The good news: the compiler already tells you the fix, and it’s almost always one word, move. This page shows the fix first, then explains why. For the other errors in this series, see Rust compiler errors.
The error message
Section titled “The error message”error[E0373]: closure may outlive the current function, but it borrows `scores`, which is owned by the current function --> src/main.rs:5:32 |5 | let handle = thread::spawn(|| { | ^^ may outlive borrowed value `scores`6 | println!("Scores: {scores:?}"); // ❌ only borrows scores | ------ `scores` is borrowed here |note: function requires argument type to outlive `'static` --> src/main.rs:5:18 |5 | let handle = thread::spawn(|| { | __________________^6 | | println!("Scores: {scores:?}"); // ❌ only borrows scores7 | | }); | |______^help: to force the closure to take ownership of `scores` (and any other referenced variables), use the `move` keyword |5 | let handle = thread::spawn(move || { | ++++This is the program that produces it:
use std::thread;
fn main() { let scores = vec![90, 85, 77]; let handle = thread::spawn(|| { println!("Scores: {scores:?}"); // ❌ only borrows scores }); handle.join().unwrap();}With async code you’ll see the same code with slightly different words: “async block may outlive the current function, but it borrows user_id”. It is the same error, and it has the same fix (async move).
What it means
Section titled “What it means”Read the labels from top to bottom:
^^ may outlive borrowed valuepoints at the closure||. Rust can’t promise the closure is finished beforemainmoves on.`scores` is borrowed herepoints at the place where the closure usesscores. Becauseprintln!only needs to read it, the closure captured a reference toscores, not the vector itself.function requires argument type to outlive 'staticexplains why that is a problem:thread::spawnonly accepts closures that can live for as long as the program needs them.'staticmeans “doesn’t borrow anything that could disappear”.
Imagine lending a friend your notebook and saying “bring it back when you’re done”, while you are about to move to another city. If your friend works slowly, the notebook has no home to return to. Rust refuses that deal and asks you to give the notebook away instead.
Why Rust does this
Section titled “Why Rust does this”A closure captures variables in the cheapest way that works. If it only reads a variable, it borrows it with &. That is great for closures that run right away, like the ones you pass to .map() or .filter(). You can read more on the Closures page.
A new thread is different. It can run for any amount of time, and main doesn’t wait for it unless you call join. If the thread held a reference to scores, and main finished first, scores would be dropped and the thread would read freed memory. That is a “dangling reference”, one of the most common bugs in C and C++ thread code. Rust stops it at compile time.
The same thing happens when you return a closure. The function’s local variables (and its parameters) are dropped when it returns. A closure that still borrows them would point at nothing.
How to fix it
Section titled “How to fix it”Fix 1: Add move (the usual answer)
Section titled “Fix 1: Add move (the usual answer)”Choose this almost every time. It’s exactly what rustc suggests. move tells the closure to take ownership of the variables it uses, so they travel with it:
use std::thread;
fn main() { let scores = vec![90, 85, 77]; let handle = thread::spawn(move || { let total: i32 = scores.iter().sum(); println!("Scores: {scores:?}, total {total}"); }); handle.join().unwrap();}Output:
Scores: [90, 85, 77], total 252The same fix works for a function that returns a closure. Without move, |x| x + n borrows the parameter n, which is gone once make_adder returns. With move, each closure keeps its own copy of n:
fn make_adder(n: i32) -> impl Fn(i32) -> i32 { move |x| x + n}
fn main() { let add_five = make_adder(5); let add_ten = make_adder(10); println!("{}", add_five(1)); println!("{}", add_ten(1));}Output:
611move does not change which closure trait you get. A move closure that only reads its data is still Fn and can be called many times.
Fix 2: Clone first, when you still need the value
Section titled “Fix 2: Clone first, when you still need the value”After move, the variable belongs to the closure. If main uses it later, you’ll get E0382 (“borrow of moved value”), and rustc suggests cloning the value before moving it. Make a copy for the closure and keep the original:
use std::thread;
fn main() { let guests = vec![String::from("Ana"), String::from("Ben")]; let guests_for_thread = guests.clone();
let handle = thread::spawn(move || { println!("Thread sees {} guests", guests_for_thread.len()); }); handle.join().unwrap();
println!("Main still has: {guests:?}");}Output:
Thread sees 2 guestsMain still has: ["Ana", "Ben"]Cloning a small list is cheap and perfectly fine. If the data is big, or many threads need it, use Arc (Fix 3) so they all share one copy.
Fix 3: Share with Arc across several threads
Section titled “Fix 3: Share with Arc across several threads”Choose this when several threads need to read the same data. Arc (“atomically reference counted”) lets many owners share one value. Arc::clone only copies a pointer and bumps a counter; the data itself is not copied:
use std::sync::Arc;use std::thread;
fn main() { let prices = Arc::new(vec![4.5, 2.0, 3.5]); let mut handles = Vec::new();
for discount in [0.0, 0.1, 0.5] { let prices = Arc::clone(&prices); // one handle per thread handles.push(thread::spawn(move || { let total: f64 = prices.iter().sum(); total * (1.0 - discount) })); }
for handle in handles { println!("Total: {:.2}", handle.join().unwrap()); } println!("Prices: {prices:?}");}Output:
Total: 10.00Total: 9.00Total: 5.00Prices: [4.5, 2.0, 3.5]Arc alone gives read-only sharing. If the threads also need to change the data, wrap it as Arc<Mutex<T>>. See Concurrency and Shared state.
Fix 4: Borrow safely with std::thread::scope
Section titled “Fix 4: Borrow safely with std::thread::scope”Choose this when the threads only need the data for a short job and you’d rather borrow than move or clone. Threads started inside thread::scope are guaranteed to finish before scope returns, so borrowing local variables is safe and no move is needed:
use std::thread;
fn main() { let scores = vec![90, 85, 77, 64]; let (first, second) = scores.split_at(2);
let (a, b) = thread::scope(|s| { let h1 = s.spawn(|| first.iter().sum::<i32>()); let h2 = s.spawn(|| second.iter().sum::<i32>()); (h1.join().unwrap(), h2.join().unwrap()) }); // both threads are done here
println!("{a} + {b} = {}", a + b); println!("Still ours: {scores:?}");}Output:
175 + 141 = 316Still ours: [90, 85, 77, 64]Scoped threads are the cleanest answer for “split the work, then use the results”. The Shared state lesson starts with them.
Where you’ll usually see it
Section titled “Where you’ll usually see it”Spawning threads in a loop. Even a plain number is borrowed if the closure only reads it:
for worker in 1..=3 { handles.push(thread::spawn(|| { println!("Worker {worker} started"); // ❌ borrows `worker` }));}Fix: thread::spawn(move || ...). worker is an i32, so move just copies it into each thread.
Returning a boxed closure. Box::new(|| greeting.clone()) inside a function that returns Box<dyn Fn() -> String> borrows the local greeting, and rustc adds note: closure is returned here. Fix: Box::new(move || greeting.clone()). More in Fn, FnMut and FnOnce.
Async tasks (Tokio and friends). tokio::spawn works like thread::spawn: the task may run long after the current function returns, so it needs 'static data. Writing tokio::spawn(async { ... }) and using a local variable inside gives “async block may outlive the current function”. Fix: tokio::spawn(async move { ... }), and clone or Arc::clone anything you still need outside the task. See Async/await.
Related errors
Section titled “Related errors”- E0382: borrow of moved value: appears right after you add
moveand then use the variable again outside the closure. Clone it first (Fix 2) or useArc(Fix 3). - E0597: borrowed value does not live long enough: a close cousin. A reference is still in use after the value it points to was dropped.
- E0515: cannot return reference to local variable: the same “it’s gone when the function returns” problem, with a plain reference instead of a closure.
- E0106: missing lifetime specifier: shows up when you try to return a reference (or a closure type) that borrows something.
- All compiler errors: the full list of error pages.
Quick check
Section titled “Quick check”Quick check
1. A thread closure prints name without using move. Why do you get E0373?
2. You add move, but main still needs to print the vector after spawning. What's the simplest fix?
3. Which tool lets threads borrow local variables without move?
Score: 0 / 3
Read more
Section titled “Read more”- E0373 — Rust error codes index: the official explanation of this error, with the
movefix. - Using Threads to Run Code Simultaneously — The Rust Book: the section “Using move Closures with Threads” walks through this exact error.
- Closures — The Rust Book: how closures capture variables by reference, by mutable reference, or by value.
movekeyword — standard library docs: whatmovedoes to a closure’s captures, with short examples.std::thread::scope— standard library docs: scoped threads that can safely borrow local data.