When a closure grabs too much
You're building a game loop. You have a Player struct holding position and health. You pass a closure to window.on_resize so the UI updates when the screen changes. The closure needs to read the player's position. You write || { println!("{}", player.x); }. The compiler rejects it. You add move. The compiler rejects it again because you still need player in the loop to update health. You're fighting the borrow checker over a closure that just wants to read a number.
This happens when closures capture variables. Rust closures grab values from their surroundings automatically. The default behavior is usually what you want, but when lifetimes clash or ownership needs to transfer, the compiler stops you. Understanding how closures capture variables turns these errors into simple fixes.
Closures capture the environment
Closures are anonymous functions that grab variables from their surroundings. Rust calls this capturing the environment. By default, a closure is lazy. It captures variables by reference. It only takes what it needs. If you read a variable, the closure borrows it immutably. If you write to it, the closure borrows it mutably. The move keyword changes the deal. It forces the closure to take ownership of the captured variables. The variables move into the closure's box. The original scope loses them.
Think of a closure like a contractor. By default, the contractor borrows tools from your workshop. They use the hammer, put it back, use the saw, put it back. You still have your tools. If the contractor needs to take the tools home, they sign a move agreement. The tools go into their truck. You can't use them anymore. If the contractor needs to modify the tools, they need exclusive access. No one else can touch the tools while they're working.
Minimal example: default capture
Closures capture by reference by default. This keeps the original variables alive in the outer scope.
fn main() {
let data = vec![1, 2, 3];
// Closure captures `data` by immutable reference by default.
// It only needs to read, so it borrows.
let read_closure = || {
println!("{:?}", data);
};
read_closure();
// `data` is still usable because the closure only borrowed it.
println!("{:?}", data);
}
The compiler analyzes the closure body. It sees data is read. It infers the capture mode is &T. It checks lifetimes. Since the closure runs and drops before data goes out of scope, the borrow is valid. The code compiles.
Trust the compiler's inference. It captures the least it needs.
How the compiler decides
The compiler looks inside the closure. It finds every variable used. For each variable, it decides the capture mode. Reading? Immutable borrow. Writing? Mutable borrow. This happens automatically. You don't annotate &data in the closure signature. The closure signature is empty ||. The capture is implicit. This is why closures are ergonomic. They do the minimal work required.
Closures are not just functions. They are anonymous structs. The compiler generates a unique type for each closure. This type holds fields for captured variables. The fields can be references or owned values. This means a closure with move has a different type than one without. This matters for generics. If a function takes impl Fn(), it accepts both. If it takes a specific type, the capture mode changes the type.
The Fn traits define what the closure can do. Fn means it can be called multiple times without mutating captured state. FnMut means it can mutate captured state. FnOnce means it consumes the closure. The compiler picks the most restrictive trait based on the body. If you move a value out, it's FnOnce. If you mutate, it's FnMut. If you only read, it's Fn.
Convention: The community follows the minimum capture principle. Only add move when the compiler forces you. Adding move unnecessarily can cause ownership errors later if you need the variable. Let the compiler borrow unless you have a reason to move.
The move keyword: taking ownership
The move keyword forces ownership transfer. When you write move || { ... }, the compiler captures every variable by value. If the variable implements Copy, like i32, it copies the value. If it doesn't, like String or Vec, it moves the value. The original variable becomes unusable.
fn main() {
let data = vec![1, 2, 3];
// `move` forces `data` to move into the closure.
// The closure now owns the vector.
let owned_closure = move || {
println!("{:?}", data);
};
owned_closure();
// `data` has moved. Using it here is impossible.
// println!("{:?}", data); // E0382: use of moved value
}
The compiler rejects the commented line with E0382 (use of moved value). The vector moved into the closure. The outer scope no longer has access. This is intentional. The closure owns the data. When the closure drops, the data drops.
Don't fight the move. If the closure needs the data, give it the data.
Realistic example: threads and async
Threads require closures that own their data. A thread might outlive the function that created it. If the closure borrows data, the borrow would dangle when the function returns. The compiler prevents this. You must use move to transfer ownership into the thread.
use std::thread;
fn main() {
let data = vec![1, 2, 3];
// `spawn` requires a closure that owns its data ('static).
// Default capture borrows `data`. The borrow can't outlive `main`.
// Uncommenting this causes a lifetime error.
// thread::spawn(|| {
// println!("{:?}", data);
// });
// `move` forces `data` to move into the closure.
// The thread now owns the vector.
thread::spawn(move || {
println!("{:?}", data);
});
// `data` is gone.
// println!("{:?}", data); // E0382
}
Async blocks follow the same rules. async { ... } captures by reference. async move { ... } captures by value. Use async move when the future needs to outlive the scope.
Convention: move is a keyword, not a trait. It changes how the closure captures variables. It does not change the Fn traits. A move closure can still be Fn, FnMut, or FnOnce depending on the body.
Pitfalls and compiler errors
Mutable capture locks the variable. If you mutate a variable inside, the closure holds a &mut. You can't read the variable outside while the closure exists. The compiler rejects this with E0502 (cannot borrow as mutable because it is also borrowed as immutable). The fix is to drop the closure or restructure the code. Don't keep a mutable borrow alive longer than necessary.
fn main() {
let mut count = 0;
// Closure captures `count` by mutable reference.
let inc = || {
count += 1;
};
// `count` is borrowed mutably by `inc`.
// println!("{}", count); // E0502
inc();
println!("{}", count); // OK: closure dropped, borrow released
}
Moving a value consumes it. If you move a String into a closure, you can't use the String afterward. The compiler rejects this with E0382 (use of moved value). If you need the data in both places, share it with Rc or Arc. Don't try to clone manually unless you need a deep copy.
Partial moves can trap you. If you have a struct and you move a field into a closure, the struct is partially moved. You can't use the struct as a whole anymore. The compiler rejects this with E0507 (cannot move out of borrowed content) if you try to move a field from a reference. Access fields directly or clone the field.
struct Config {
name: String,
level: i32,
}
fn main() {
let config = Config {
name: String::from("debug"),
level: 1,
};
// `move` captures `config` by value.
// The whole struct moves.
let log = move || {
println!("Config: {}", config.name);
};
log();
// `config` is moved.
// println!("{}", config.level); // E0382
}
If you only need one field, capture that field. Don't move the whole struct.
fn main() {
let config = Config {
name: String::from("debug"),
level: 1,
};
// Capture only `name` by moving it.
// `config` remains usable for other fields.
let name = config.name;
let log = move || {
println!("Config: {}", name);
};
log();
println!("Level: {}", config.level); // OK
}
Treat move as a transfer of responsibility. Once you move, the outer scope is done.
Decision: when to use default capture vs move
Use default capture when the closure executes within the same scope and variables remain valid. The compiler handles the borrowing automatically. Use move when the closure is sent to another thread or stored in a data structure that outlives the current scope. The closure must own its data to survive. Use move when you want to transfer ownership of a value into the closure and stop using the variable in the outer scope. Use Rc<T> or Arc<T> when the closure needs to share data with the outer scope without taking ownership, and the data must survive the scope. Wrap the data in the smart pointer before capturing.