Rust — error[E0373]: closure may outlive the current function
Fix E0373 closure may outlive the current function by understanding borrowed captures versus move semantics, and pass ownership or clone into long-lived closures.
Symptoms
- error[E0373]: closure may outlive the current function, but it borrows x with a note about the borrowed variable
- The closure is passed to spawn, a framework handler, or stored in a struct that outlives the call
- Adding move compiles but shifts the error to a borrow of a moved value elsewhere
Possible causes
- A closure passed to thread::spawn or stored long-term borrows local variables by reference
- The compiler suggests adding the move keyword, but moving conflicts with later uses in the same function
- A framework stores the closure in a registry, so its captures must be owned, not borrowed from stack locals
- Shared references intended for reading need lifetime extension via Rc/Arc clones instead of move
Diagnosis steps
Read the borrow note
cargo check 2>&1 | grep -B2 -A10 E0373Interpretation
The note names the borrowed variable and where the closure escapes — the exact pair of lifetime ends that conflict.
Decide ownership versus sharing
cargo check 2>&1 | grep "consider adding" | head -3Interpretation
move works when the function no longer needs the value; Arc/Rc clones work when both the closure and the caller need access.
Verify both users compile
cargo check && cargo testInterpretation
After move or clone, the closure owns its captures and the original scope keeps what it needs — the test suite proves semantics survived.
Root cause
A closure captures variables by reference by default, tying its lifetime to the borrowed locals. When the closure escapes the function — into a thread, a handler registry, a struct field — those locals would be gone while the closure still references them, so the compiler demands either ownership (move) or shared ownership (clone into Arc/Rc).
Fix
Add move when the function can give up ownership, clone small values into the closure when the caller still needs them, use Arc for shared state captured by long-lived closures, and restructure APIs that ask for closures to accept owned data plus function pointers where appropriate.
Risk note
move changes Drop timing and can surprise code that relies on the original binding after the closure. Review uses after the move point.
Verify the fix
The escaping closure compiles; tests confirm the closure executes correctly after the original function returned, and the caller-side uses still work.
Symptom walkthrough
The error names the escape and the borrow in one note:
error[E0373]: closure may outlive the current function,
but it borrows `config`
--> src/worker.rs:12:24
|
12 | let task = spawn(move || use(&config));
| ------- ^- `config` is borrowed here
| |
| this closure may outlive the current function
The conflict is between two lifetimes: the closure is stored somewhere that outlives this call (spawn, a handler table, a struct), while its capture borrows a local that dies at function return. The compiler suggests move; the subtlety is that move changes what the caller can still do with the value afterward — sometimes the right fix is a clone or an Arc instead.
Fix in practice
Three shapes by need. Caller done with the value — plain move:
let task = thread::spawn(move || process(config)); // ownership travels
Caller still needs it — clone in:
let snapshot = config.clone();
let task = thread::spawn(move || process(snapshot));
use(&config); // caller keeps its copy
Both need it long-term — shared ownership:
let shared = Arc::new(config);
let for_task = Arc::clone(&shared);
let task = thread::spawn(move || process(for_task));
use(&*shared);
For framework handler registrations where the closure lives as long as the app, treat owned captures as the design default: everything the closure needs is either moved in, cloned in, or shared via Arc — never borrowed from a request-scoped stack.