The closure is outliving its data
You're writing a callback for a button click. The button needs to read a configuration string defined in the function. You write the closure, pass it to the button, and the compiler throws a fit. "closure may outlive the current function, but it borrows config." You stare at the code. The closure just prints a string. Why does the compiler think the string is going to vanish?
The string is right there in the function. The problem isn't the string. The problem is the timeline. The function is about to return. The local variables are about to be destroyed. The closure is about to be stored in the button and fired later. If the closure holds a reference to the string, that reference points to memory that will be reclaimed the moment the function ends. The compiler stops you before you create a dangling pointer.
Trust the error. The compiler has already found the dangling reference before your code even runs.
How closures capture data
Rust closures capture variables by reference by default. When you write a closure that uses a variable from the surrounding scope, the compiler generates a hidden struct that stores references to those variables. The closure borrows the data. It does not own it.
This works perfectly when the closure runs immediately and finishes before the function returns. The references are valid the entire time the closure exists. The moment you try to return the closure, or pass it to a thread, or store it in a struct, the lifetime mismatch appears. The closure wants to live longer than the data it references.
The fix is the move keyword. Adding move changes how the closure captures variables. Instead of storing references, the closure takes ownership of the variables. The data moves into the closure's hidden struct. The closure now owns the data. The function can return safely because the data travels with the closure.
Picture a messenger. You hand the messenger a note to deliver. The note is sitting on your desk. The messenger runs off to do the job. If you burn your desk before the messenger finishes, the messenger is holding a reference to ash. move is packing the note into the messenger's bag. Now the messenger owns the note. Burning your desk doesn't matter.
Don't leave the messenger holding a reference to a note you're about to shred.
Minimal example
Here is the error in action. The function make_handler tries to return a closure that uses a local variable.
fn make_handler() -> impl Fn() {
let data = vec![1, 2, 3];
// This closure captures `data` by reference.
// The compiler generates a struct with a field like `data: &Vec<i32>`.
let closure = || {
println!("{:?}", data);
};
// E0373: closure may outlive the current function, but it borrows `data`
closure
}
The compiler rejects this with E0373. The closure borrows data, but data lives only inside make_handler. Returning the closure would create a reference to a variable that ceases to exist.
Add move to force ownership transfer.
fn make_handler() -> impl Fn() {
let data = vec![1, 2, 3];
// `move` forces the closure to take ownership of `data`.
// The compiler generates a struct with a field like `data: Vec<i32>`.
let closure = move || {
println!("{:?}", data);
};
// `data` is now inside the closure.
// The closure owns the Vec. It can be returned safely.
closure
}
The error vanishes. The closure carries its own luggage.
Under the hood: The closure struct
Rust closures are not magic. The compiler generates a unique struct for each closure. The fields of this struct are the captured variables. The move keyword changes the types of these fields.
Without move, the compiler infers the cheapest capture mode. If the closure only reads the variable, it captures &T. If it mutates the variable, it captures &mut T. The struct holds references. The lifetime of the struct is tied to the lifetime of the referenced data.
With move, the compiler forces the fields to be owned values. The struct holds T instead of &T. The data is moved into the struct. The lifetime of the struct is now independent of the original scope. The struct owns the data, so the struct can live as long as the data lives.
This structural change explains why move fixes the error. The closure type changes from "I need a reference to data" to "I contain data". The compiler can verify that the closure is self-contained.
Convention aside: When you see move in production code, it usually signals that the closure is being stored or returned. If the closure runs inline, move is rarely needed. The keyword is a lifetime fix, not a performance tweak.
Realistic example: Threads and async tasks
The most common place you'll encounter this error is when spawning threads or async tasks. Runtimes like std::thread or tokio take ownership of the closure and run it on a separate execution context. The closure must own everything it uses.
use std::thread;
fn spawn_worker() {
let config = String::from("high_priority");
// `thread::spawn` takes ownership of the closure.
// The closure runs on a new thread that lives independently.
thread::spawn(move || {
// `config` is moved into the closure.
// The thread owns the String.
println!("Worker config: {}", config);
});
// `config` has been moved.
// println!("{}", config); // Error E0382: use of moved value.
}
The move keyword is mandatory here. The thread might outlive the function that spawned it. The closure must own config. Without move, the closure would borrow config, and the borrow would dangle as soon as spawn_worker returns.
Convention aside: In async code, move is almost always required for tokio::spawn or async move { ... } blocks. The async runtime stores the future and polls it later. The future must own its data. If you write a closure without move in a spawn context, it's a bug waiting to happen.
Check your usage after the move. If you try to use the variable again, the compiler will remind you that you just gave it away.
Pitfalls and edge cases
Move consumes the variable
move transfers ownership. If the variable is used after the closure, you get E0382 (use of moved value). This is intentional. The closure owns the data. The original scope cannot use it.
fn example() {
let data = vec![1, 2, 3];
let closure = move || {
println!("{:?}", data);
};
// E0382: use of moved value: `data`
// println!("{:?}", data);
}
If you need to use the variable in both the closure and the outer scope, you need shared ownership. Wrap the data in Rc or Arc before moving.
Move on references
move moves the captured variable, not the target of the variable. If you capture a reference, move moves the reference. The reference still points to the original data.
fn example() {
let data = vec![1, 2, 3];
let ref_data = &data;
// This closure moves `ref_data`.
// It owns the reference, but the reference points to `data`.
let closure = move || {
println!("{:?}", ref_data);
};
// `data` is still alive here, so the reference is valid.
// But if `data` were to drop, the closure would hold a dangling reference.
closure();
}
This is a subtle trap. move does not deep-copy the data. It moves whatever is captured. If you capture a reference, you move the reference. The closure still depends on the lifetime of the original data. Use move on the owned value, not on a reference to it, if you want the closure to be independent.
Move and Copy types
move respects the Copy trait. If a variable implements Copy, move copies the value instead of moving it. The original variable remains usable.
fn example() {
let count = 42;
// `i32` implements `Copy`.
// `move` copies the value into the closure.
let closure = move || {
println!("{}", count);
};
// `count` is still usable.
println!("{}", count);
}
This behavior prevents unnecessary moves for primitive types. Integers, booleans, and characters are Copy. Strings, vectors, and custom structs are not. The compiler handles this automatically. You don't need to check for Copy manually.
Decision matrix
Use move when the closure is returned from a function or stored in a struct that lives longer than the current scope. Use move when spawning threads or async tasks where the runtime takes ownership of the closure and runs it independently. Use default capture when the closure executes and completes entirely within the current function, before any local variables go out of scope. Use Rc or Arc inside a move closure when you need to share data between the closure and the outer scope, or between multiple closures.
Pick the capture mode that matches the lifetime of the data, not the convenience of the syntax.