July 8, 20254 min

What are move closures (move || { ... })? When are they necessary, and how do they interact with ownership?

m
mayo

A move closure (defined with the move keyword) forces the closure to take ownership of variables it captures from the environment. Unlike regular closures, which capture variables by reference (immutable or mutable) when possible, move closures move or copy the variables into the closure itself.

Key Mechanics

1. Ownership Transfer

  • For non-Copy types (e.g., String, Vec), the closure takes ownership of the variable:

    let s = String::from("hello");
    let closure = move || println!("{}", s); // `s` is moved into the closure
    // println!("{}", s); // ERROR: `s` was moved
    
  • For Copy types (e.g., i32, bool), the closure copies the value:

    let x = 42;
    let closure = move || println!("{}", x); // `x` is copied
    println!("{}", x); // OK: `x` is still valid
    

2. Interaction with Closure Traits

A move closure’s trait (Fn, FnMut, FnOnce) depends on how the captured variables are used:

  • Fn: Read-only access to captured variables.
  • FnMut: Mutates captured variables.
  • FnOnce: Consumes captured variables (e.g., drop).

When Are Move Closures Necessary?

1. Closures Outliving Their Environment

When a closure is used in a different scope (e.g., a thread or async task), it must own its data to avoid dangling references:

use std::thread;

let data = String::from("thread-safe");
thread::spawn(move || { // `move` forces ownership of `data`
    println!("{}", data); // Safe: `data` lives in the closure
}).join().unwrap();

2. Breaking Reference Cycles

If a closure needs to capture a value that’s also borrowed elsewhere, move ensures ownership is transferred:

let mut vec = vec![1, 2, 3];
let closure = move || { // Takes ownership of `vec`
    // vec.push(4); // ERROR: `vec` is moved (can’t mutate)
};
// vec.push(4); // ERROR: `vec` is moved into closure

3. Explicit Ownership Control

When you want to avoid accidental borrows or force a copy:

let x = 42;
let closure = || println!("{}", x); // Borrows `x`
let move_closure = move || println!("{}", x); // Copies `x` (since `i32` is `Copy`)

Examples

1. Non-Copy Type (Ownership Moved)

let s = String::from("hello");
let closure = move || println!("{}", s);
closure(); // Works: closure owns `s`
// closure(); // ERROR if `s` is consumed (e.g., `FnOnce`)

2. Copy Type (Value Copied)

let x = 42;
let closure = move || x + 1; // Copies `x`
println!("{}", x); // OK: `x` is `Copy`

3. Mixing move and Mutation

let mut count = 0;
let mut closure = move || { // `count` is copied (since `i32` is `Copy`)
    count += 1; // Operates on the copied `count`
    count
};
println!("{}", closure()); // 1
println!("{}", closure()); // 2
println!("{}", count); // 0 (original unchanged)

Pitfalls

  • Unintended Moves:

    let s = String::from("hello");
    let _ = move || println!("{}", s); // `s` moved here
    // println!("{}", s); // ERROR: `s` is gone
    
  • Overusing move: Unnecessary copies/moves can hurt performance or cause compile errors.

Key Takeaways

Use move closures when:

  • The closure outlives its environment (e.g., threads).
  • You need explicit ownership to avoid borrow checker issues.

Avoid move for:

  • Local, short-lived closures that don’t escape their scope.
  • Copy types where borrowing is sufficient.

Try This: What happens if you use move with a closure that captures a mutable reference (&mut T)?
Answer: The reference itself is moved (but the data it points to isn’t owned). This is rarely useful and may lead to lifetime errors!