Introduction
By default, closures capture variables using the least restrictive borrowing mode needed. The move keyword overrides this default and forces the closure to take ownership of all captured variables. Understanding when to use move is essential for threading, returning closures, and async programming.
Key Concepts
- Default capturing: The compiler picks
&T,&mut T, orT(move) for each captured variable based on how the closure uses it. movekeyword: Forces all captured variables to be moved (or copied, forCopytypes) into the closure.- Copy types with
move: For types that implementCopy(likei32,bool,f64),movecopies the value rather than transferring ownership. The original remains usable.
Real World Context
You will reach for move closures in three common scenarios: spawning threads (the closure must own its data because the thread may outlive the caller), returning closures from functions (captured locals would be dangling references without move), and async blocks (the future may be polled after the surrounding scope ends).
Deep Dive
Default capture behavior
Without move, Rust borrows by the least powerful mode needed:
rustlet name = String::from("Alice"); let greet = || println!("Hello, {name}"); // Borrows &name greet(); println!("Still valid: {name}"); // name is still accessible
The closure only needs to read name, so it captures an immutable reference. The original name remains usable after the closure.
Forcing ownership with move
The move keyword transfers ownership into the closure:
rustlet name = String::from("Alice"); let greet = move || println!("Hello, {name}"); // Owns name now greet(); // println!("{name}"); // Error: name was moved into the closure
After the move, the outer scope no longer owns name.
Thread spawning requires move
Spawned threads may outlive the scope that created them, so they must own their data:
rustuse std::thread; let data = vec![1, 2, 3, 4, 5]; let handle = thread::spawn(move || { let sum: i32 = data.iter().sum(); println!("Sum: {sum}"); // Output: Sum: 15 }); handle.join().unwrap(); // data is no longer accessible here
Without move, the compiler would reject this code because data could be dropped before the thread finishes.
Returning closures from functions
When a function returns a closure, any captured locals would be destroyed at the end of the function. move ensures the closure owns them:
rustfn make_adder(base: i32) -> impl Fn(i32) -> i32 { move |x| x + base // base is moved (copied, since i32 is Copy) } let add_ten = make_adder(10); assert_eq!(add_ten(5), 15); assert_eq!(add_ten(20), 30);
Since i32 implements Copy, base is copied into the closure rather than moved.
Move with Copy types
For Copy types, move copies rather than moves — the original remains valid:
rustlet threshold = 42; // i32 is Copy let check = move || threshold > 0; // threshold is copied println!("Original still valid: {threshold}"); // Works fine
This is an important distinction. move does not always invalidate the original binding.
Common Pitfalls
- Using
movewhen a borrow would suffice —movetransfers ownership of all captured variables. If you only need to move one variable, consider cloning the others first and usingmoveon the clone. - Expecting
moveto deep-clone —movetransfers ownership; it does not clone. If you need the original value after the closure, clone before the closure. - Forgetting that
Copytypes survivemove— Integers, booleans, and otherCopytypes are copied, not moved. The original is still valid.
Best Practices
- Clone-then-move for selective ownership — When only some captured variables need to be owned, clone the ones you want to keep and let
movetake the clones. - Always use
movewiththread::spawn— The compiler usually requires it anyway, but being explicit documents intent. - Prefer
moveforasyncblocks — Async blocks often outlive their enclosing scope. Usingmoveprevents subtle lifetime errors.
Summary
- Default closures borrow by the least restrictive mode needed.
moveforces the closure to take ownership of all captures.Copytypes are copied, not moved — the original remains valid.moveis essential for threads, returned closures, and async blocks.- Rust 1.94 makes per-field closure capturing more precise, reducing unnecessary moves.
Code Examples
use std::thread;
// Clone-then-move pattern for selective ownership
fn parallel_search(haystack: &[String], needle: String) -> bool {
let mid = haystack.len() / 2;
let (left, right) = haystack.split_at(mid);
// Clone data for the spawned thread
let left_owned = left.to_vec();
let needle_clone = needle.clone();
let handle = thread::spawn(move || {
left_owned.iter().any(|item| item.contains(&needle_clone))
});
// Main thread searches the right half with the original needle
let right_found = right.iter().any(|item| item.contains(&needle));
let left_found = handle.join().unwrap();
left_found || right_found
}
// Returning a closure that owns its state
fn make_counter(start: u32) -> impl FnMut() -> u32 {
let mut current = start;
move || {
let value = current;
current += 1;
value
}
}
fn main() {
let mut counter = make_counter(10);
assert_eq!(counter(), 10);
assert_eq!(counter(), 11);
assert_eq!(counter(), 12);
}