When the closure needs to travel
You're building a chat server. The main loop reads a message, creates a new thread to process it, and passes the message text into that thread. You write the code, hit run, and the compiler stops you. It says the closure might outlive the borrowed data. You didn't ask for a thread to live longer than the message; you just wanted to send the message off to work. The fix is one word: move.
Add move and the compiler stops screaming. You just transferred ownership.
Closures capture by default
Rust closures capture variables from their environment. Without move, the compiler tries to be helpful by borrowing those variables. The closure holds a reference. It points back to the data where it was created. This is efficient. You get a closure that reads the data without copying it.
move changes the rules. It forces the closure to take ownership of everything it captures. The values move into the closure's storage. The outer scope loses access. The closure becomes self-contained.
Think of a closure as a recipe card. By default, the recipe points to ingredients sitting on your kitchen counter. If you hand that recipe to a friend in another room, the ingredients stay on your counter. The recipe is useless to your friend because they can't reach the counter. Adding move is like packing the ingredients into a box along with the recipe. Your friend gets the box. They have the ingredients. You no longer have them.
The closure owns the box. You hold nothing but the closure.
Minimal example
/// Demonstrates move closure taking ownership of a String.
fn main() {
let data = String::from("secret");
// The closure moves `data` into itself.
// `data` is no longer usable in `main`.
let worker = move || {
println!("Worker has: {}", data);
};
// Calling the closure works fine.
worker();
// This line would fail with E0382.
// `data` was moved into `worker`.
// println!("{}", data);
}
The move keyword sits right before the ||. It applies to the entire capture. Every variable used inside the closure moves in. If you use multiple variables, they all move.
How move changes the type
Closures are not functions. The compiler generates a unique anonymous struct for each closure. The struct fields store the captured variables. A normal closure stores references. A move closure stores the values themselves.
This changes the type of the closure. A borrowing closure and a move closure have different types. The compiler treats them as distinct structs. This matters when you pass closures to generic functions or store them in data structures.
When you write move, the compiler generates a struct with owned fields. At runtime, the data lives inside the closure's memory. When the closure is called, it accesses its own data. When the closure is dropped, the data is dropped too.
The closure is the owner. Treat it like one.
Move versus copy
move doesn't always mean a deep copy. If the type implements Copy, the compiler generates a bitwise copy. i32, bool, &str, and f64 are Copy. Moving them is cheap. The original variable stays valid because the value was duplicated.
String, Vec, and Box are not Copy. Moving them transfers the pointer, length, and capacity. The old variable is invalidated. The compiler tracks this transfer. You can't use the old variable after the move. This is why move is safe. The compiler enforces the transfer.
If you move a String into a closure, the closure owns the heap allocation. The outer scope holds a dead variable. If you move an i32, both the closure and the outer scope have valid integers.
Realistic example: threads
Threads are the most common reason to use move. A thread may outlive the function that created it. The closure passed to thread::spawn must own its data. Borrowing is not allowed because the borrowed data might be dropped before the thread finishes.
use std::thread;
/// Spawns a thread that owns its configuration.
fn main() {
let config = String::from("production");
// Thread requires 'static lifetime.
// `move` ensures the closure owns `config`.
let handle = thread::spawn(move || {
println!("Running in: {}", config);
});
// `config` is moved. Cannot use here.
// println!("{}", config); // E0382
handle.join().unwrap();
}
The thread::spawn function takes a closure that must be 'static. This means the closure cannot hold any borrowed references with a limited lifetime. move satisfies this requirement by transferring ownership. The closure owns the data. The data lives as long as the closure.
The community convention is simple: always use move with thread::spawn. It's the standard pattern. You rarely see a thread spawn without move. If you forget it, the compiler error is your reminder.
Spawn threads with move. It's the law of the land.
Pitfalls and scope traps
move captures every variable in scope. It doesn't care if you only use one. If you have a database connection in scope and write move || { println!("hi"); }, the connection moves into the closure. You lose the connection in the outer scope. The compiler catches this. You need to be careful about what's in scope when you write move.
If you try to use a variable after moving it into a closure, the compiler rejects you with E0382 (use of moved value). This error appears when you forget that move transfers ownership. The variable is gone from the outer scope.
Another trap is unused variables. If a variable is in scope but not used inside the closure, move still captures it. If that variable doesn't implement Copy, you lose access to it. The closure swallows the variable. You can't get it back. This is a common source of confusion. The closure captures the whole environment, not just the variables you reference.
Scope is your friend. Shrink the scope before you move.
If you need move but have other variables in scope that you want to keep, wrap the closure in a block. The block limits the scope. move only captures what's inside the block. This is a standard pattern to avoid dragging in unwanted data.
/// Limits capture scope using a block.
fn main() {
let keep_this = String::from("important");
let big_data = String::from("huge payload");
// Block limits the scope.
// `move` only captures `needed`.
{
let needed = String::from("data");
let c = move || {
println!("{}", needed);
};
c();
}
// `big_data` and `keep_this` are still usable.
println!("Kept: {}", keep_this);
}
The block creates a new scope. Variables declared inside the block are not visible outside. move captures only what's in scope. This keeps your outer variables safe.
When to use move
Use move when spawning threads because the thread may outlive the function that created it, and the closure must own its data. Use move when passing a closure to a callback that stores it for later execution, since the original variables might be dropped before the callback runs. Use move when you want to transfer ownership of a value into a closure and stop using it in the outer scope. Reach for a borrowing closure when the data lives longer than the closure and you need to read the same value from multiple places without copying. Reach for Rc or Arc when multiple closures need to share ownership of the same data, and you can't move it into just one of them.
Move ownership when you can. Share only when you must.