The closure grabs exactly what it needs, nothing more
You write a function that returns a closure. You expect the closure to remember the local variables, just like in JavaScript or Python. You compile, and the compiler rejects you with E0382. The variable moved into the closure, and you can't touch it anymore. Or you try to mutate a captured variable and get E0596. Rust closures don't capture everything blindly. They capture exactly what they need, and they capture it in the cheapest way possible. This precision is what makes Rust fast, but it trips up anyone coming from languages where closures are magic black boxes.
The contractor analogy
Think of a closure like a contractor you hire to do a job inside your house. You have tools in your garage. The contractor needs a drill. In many languages, the contractor just takes the drill. Period. Rust's contractor is different. The contractor looks at the job description. If the job only requires checking the drill's serial number, the contractor glances at the drill and leaves it in the garage. If the job requires drilling a hole, the contractor borrows the drill, uses it, and puts it back. If the job requires taking the drill to a different house, the contractor takes ownership of the drill.
The compiler reads how you use the variable inside the closure and decides the capture mode automatically. This is called inference. The closure captures by reference by default, and only moves when forced. The compiler picks the cheapest capture mode that satisfies the code. Trust the inference, but watch out when the closure outlives the scope.
Minimal example: inference in action
fn main() {
let x = 5;
// The closure only reads `x`.
// The compiler infers an immutable borrow.
// `x` is not moved.
let read_x = || println!("{}", x);
read_x();
// `x` is still available because it was borrowed, not moved.
println!("x is still here: {}", x);
}
The variable x lives on the stack. The closure read_x captures a reference to x. When read_x runs, it reads through that reference. The original x remains valid. The compiler chose an immutable borrow because that's all the closure needed.
Realistic example: the mutable borrow trap
Closures often appear in iterator chains or callback handlers. When a closure mutates a captured variable, the compiler infers a mutable borrow. This creates a borrow that lasts as long as the closure exists.
fn main() {
let mut data = vec![1, 2, 3];
// The closure mutates `data`.
// The compiler infers a mutable borrow.
// The closure holds `&mut data` for its lifetime.
let modify = || {
data.push(4);
};
// This line fails. `data` is borrowed mutably by `modify`.
// The compiler rejects this with E0502.
// println!("{:?}", data);
modify();
// `data` is accessible again after `modify` is dropped or used?
// No, `modify` still holds the borrow until it goes out of scope.
// println!("{:?}", data); // E0502
}
The closure modify holds a mutable borrow of data. As long as modify is alive, you cannot access data in any way. This is the "long borrow" problem. The borrow doesn't end when the closure is called; it ends when the closure variable goes out of scope. If you need to use data while the closure exists, you must restructure the code. Drop the closure early, or avoid capturing the mutable reference.
The three capture traits
Rust defines three traits that describe how a closure captures and uses variables. The compiler infers which trait your closure implements based on the body.
Fn: The closure captures by immutable borrow. It can be called multiple times. It only reads captured data.FnMut: The closure captures by mutable borrow. It can be called multiple times. It mutates captured data.FnOnce: The closure captures by move. It can only be called once. It consumes captured data.
The inference follows the usage. If you only read, it's Fn. If you mutate, it's FnMut. If you move a non-Copy value, it's FnOnce.
fn main() {
let s = String::from("unique");
// This closure moves `s`.
// `s` does not implement `Copy`.
// The closure implements `FnOnce`.
let consume = || {
println!("Consumed: {}", s);
};
consume();
// consume(); // Error: closure can only be called once.
}
The closure consume takes ownership of s. After the first call, s is gone. The closure cannot be called again. This is the most restrictive capture mode. Use FnOnce when the closure must take ownership and the value cannot be shared.
The move keyword: ownership, not callability
The move keyword forces the closure to capture variables by value. It overrides the default inference of borrowing. This is essential when the closure must outlive the stack frame, such as when spawning a thread.
use std::thread;
fn main() {
let data = String::from("thread data");
// `move` forces the closure to take ownership of `data`.
// The thread might outlive `main`, so borrowing is impossible.
let handle = thread::spawn(move || {
println!("Thread sees: {}", data);
});
// `data` is moved into the closure.
// println!("{}", data); // E0382
handle.join().unwrap();
}
The move keyword changes how the closure grabs variables. It does not change how many times the closure can be called. A move closure can still implement Fn if it only reads the moved data.
fn main() {
let s = String::from("data");
// `move` forces ownership transfer.
// The closure only reads `s`, so it implements `Fn`.
let read_moved = move || println!("{}", s);
read_moved();
read_moved(); // Works! `move` didn't prevent multiple calls.
}
Developers often confuse move with FnOnce. They are orthogonal. move controls the capture. FnOnce controls callability. You can have a move closure that implements Fn. This happens when you move a Copy type, or when you move a value but only read it inside the closure. A move closure is not single-use by default.
Pitfalls and compiler errors
Closures capture the minimum. This causes friction when you expect different behavior.
If you use a type that doesn't implement Copy, the closure might move it. If you try to use the variable after the closure, the compiler rejects you with E0382. The value moved into the closure's state. Fix this by borrowing explicitly, or by wrapping the value in Rc or Arc if multiple owners are needed.
If you try to mutate a captured variable, but the variable isn't declared mut, the compiler rejects you with E0596. The closure needs a mutable borrow, but the source variable is immutable. Declare the variable mut in the outer scope.
If you try to access a variable while a closure holds a conflicting borrow, the compiler rejects you with E0502. The closure holds a borrow for its lifetime. You cannot use the variable while the closure is alive. Drop the closure early, or restructure the code to avoid the conflict.
When you pass a closure to a generic function, the function might require a specific trait bound. If the closure captures by move but the function requires Fn, the compiler rejects you with E0277. The closure doesn't satisfy the trait bound. Check the capture mode and the function's requirements.
Convention aside: when writing generic functions that take closures, prefer Fn or FnMut bounds. FnOnce is restrictive and prevents the closure from being called multiple times. Only use FnOnce when the closure must consume captured data.
Convention aside: in iterator chains, closures are usually inferred as Fn. This allows the iterator to be reused or the closure to be called multiple times. If you accidentally move a non-Copy value into an iterator closure, the chain might break. Use move only when necessary.
Decision: when to use each capture mode
Use plain closures || when the closure lives shorter than the captured variables and you only need to read or mutate them temporarily. The compiler infers the cheapest borrow, and the variables remain accessible after the closure is called.
Use move closures move || when the closure must outlive the stack frame, such as when spawning a thread or storing the closure in a struct that lives longer than the variables. The move keyword forces ownership transfer, ensuring the data stays alive.
Use Copy types like integers, booleans, or references for captured variables when you want to avoid borrow conflicts entirely. The compiler copies the value, leaving the original untouched. This is the simplest way to share data with closures.
Use Rc<T> or Arc<T> when multiple closures need to share ownership of a value. Wrap the value in a reference-counted smart pointer so the closure captures a clone of the pointer, not the value itself. This allows multiple closures to hold the data without moving it.
Use FnOnce closures when the closure must consume captured data and can only be called once. This is rare in application code but useful for one-shot operations like initializing a resource or moving a unique handle.
Pick the capture mode that matches the lifetime requirements. move is your escape hatch for threads, but reach for references first. If the compiler complains about a moved value, check if you can make the type Copy or wrap it in Rc. Fighting the move semantics usually leads to refactoring.