Skip to content

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.

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 scores
7 | | });
| |______^
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).

Read the labels from top to bottom:

  1. ^^ may outlive borrowed value points at the closure ||. Rust can’t promise the closure is finished before main moves on.
  2. `scores` is borrowed here points at the place where the closure uses scores. Because println! only needs to read it, the closure captured a reference to scores, not the vector itself.
  3. function requires argument type to outlive 'static explains why that is a problem: thread::spawn only accepts closures that can live for as long as the program needs them. 'static means “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.

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.

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 252
▶ Try it Yourself

The 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:

6
11
▶ Try it Yourself

move 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 guests
Main still has: ["Ana", "Ben"]
▶ Try it Yourself

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.00
Total: 9.00
Total: 5.00
Prices: [4.5, 2.0, 3.5]
▶ Try it Yourself

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 = 316
Still ours: [90, 85, 77, 64]
▶ Try it Yourself

Scoped threads are the cleanest answer for “split the work, then use the results”. The Shared state lesson starts with them.

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.

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