Closures That Borrow and That Steal (move Closures)

Understand how Rust closures capture variables by borrowing or taking ownership, when to use the move keyword, and how to avoid common borrow-checking pitfalls with captured state.

A closure in Rust is a piece of code that can remember the variables around it. The way it remembers them—by borrowing or by taking ownership—controls how you can use both the closure and the variables after it is defined. Rust calls this capturing. The default behavior tries to borrow as little as possible, but sometimes you need the closure to own the data outright. That is what the move keyword does.

This distinction is not just a detail; it determines whether your closure can outlive the function that created it, whether you can still use a variable after passing it to a closure, and why some iterator chains work while others refuse to compile.

How Closures Borrow by Default

When a closure body uses a variable from outside without the move keyword, Rust looks at how the variable is used inside the closure and borrows it accordingly.

If the closure only reads the variable, it captures an immutable reference (&T).

let x = 10;
let print_x = || println!("{}", x);
print_x();
println!("{}", x); // still fine: x was only borrowed immutably

The closure print_x holds an &i32 to x. That borrow lasts as long as the closure exists. Because the borrow is shared, you can still read x outside, and you can call print_x multiple times.

If the closure modifies the variable, Rust captures a mutable reference (&mut T). This requires the variable itself to be declared mut.

let mut counter = 0;
let mut increment = || counter += 1;
increment();
increment();
println!("{}", counter); // prints 2

Here increment holds an &mut counter. While that mutable borrow exists, no other borrow—mutable or immutable—can touch counter. That is the same borrow-checker rule that applies everywhere else in Rust. The closure is not special; it is just a value that holds a reference.

Mutable borrows are exclusive:

If a closure captures a mutable reference to a variable, you cannot read or write that variable through any other path until the closure is dropped or its borrow ends. Attempting to create a second borrow while the closure is alive will produce a compiler error.

let mut n = 1;
let mut add = || n += 1;
let r = &n; // error: cannot borrow `n` as immutable because it is also borrowed as mutable
add();

The mutable borrow from the closure must end before any other borrow can begin. If you need to read the value and also mutate it through a closure, you must restructure the code so the closure’s borrow does not overlap with the other use.

When Borrowing Is Not Enough

Borrowing is the least intrusive way to capture a variable, but it imposes a lifetime constraint: the closure cannot outlive the borrowed data. A function that tries to return a closure that borrows a local variable will fail.

fn make_greeter(name: &str) -> impl Fn() -> String {
    let greeting = format!("Hello, {}", name);
    || greeting  // error: closure borrows `greeting`, which does not live long enough
}

The closure wants to use greeting by reference, but greeting is dropped at the end of make_greeter. The closure cannot hold a reference to data that no longer exists. To return a closure that still knows the greeting, the closure must own the string.

fn make_greeter(name: &str) -> impl Fn() -> String {
    let greeting = format!("Hello, {}", name);
    move || greeting  // ownership of `greeting` moves into the closure
}

The move keyword forces the closure to take ownership of all the variables it uses from the surrounding scope. Now greeting lives inside the closure and stays alive for as long as the closure itself.

How move Closures Work

Adding move before the parameter pipes changes the capture mode from borrow to ownership transfer. Every variable the closure body references is moved—or copied, if the type implements Copy—into the closure’s hidden environment.

For a non-Copy type like String, the original variable becomes unusable after the closure definition.

let name = String::from("Rust");
let consume = move || println!("{}", name);
consume();
// println!("{}", name); // error: value moved into the closure

For a Copy type like i32, the closure gets its own copy. The original variable remains untouched.

let num = 42;
let show = move || println!("{}", num);
show();
println!("{}", num); // still works: num was copied, not moved

This difference trips up many newcomers who expect move to always “steal” the original, but the semantics follow Rust’s normal ownership rules: moving an i32 is just a copy.

move does not mean 'always move':

For Copy types, the original variable is still usable after a move closure captures it. This is consistent with Rust’s move semantics but can be surprising the first time you encounter it.

A move closure does not automatically become callable only once. Whether a closure is FnOnce, FnMut, or Fn is determined by what the closure body does with the captured data, not by the move keyword itself. If the closure only reads the moved values (no mutation, no consumption), it can implement Fn and be called repeatedly.

let data = vec![1, 2, 3];
let reader = move || data.len(); // moved, but only reads length
println!("{}", reader());
println!("{}", reader()); // called again without issue

Here data is moved into the closure, but the closure never drops or mutates it. The closure implements Fn and can be invoked many times. If the body consumed data (by passing it to drop, for instance), the closure would implement FnOnce and could be called only once.

The Real Reason move Exists: Escaping Scopes

The most common motivation for move is sending a closure somewhere that will outlive the current stack frame. Spawning a thread is the classic case.

use std::thread;
let message = String::from("from another thread");
let handle = thread::spawn(move || {
    println!("{}", message);
});
handle.join().unwrap();

thread::spawn requires a closure that is 'static—it must not borrow any data with a limited lifetime. By using move, we give ownership of message to the closure. The closure can then be sent to the new thread without any dangling references.

The same pattern appears in async runtimes, callback registration, and any API that stores a closure for later execution.

Correct use of move for threading:

When the compiler says a closure “may outlive the current function” and asks for move, giving ownership is the right path. This is not a workaround; it is the intended way to tell the compiler that the closure will manage the data independently.

Whole-Variable Capture and Its Workarounds

Closures in stable Rust capture entire local variables, not individual fields or subpaths. This can cause the borrow checker to reject code that logically should work because only a small part of a struct is used.

Consider a struct with two fields where one method borrows a field and another method tries to mutate a different field through a closure.

use std::collections::HashMap;
struct Context {
    input: HashMap<String, u32>,
    output: Vec<u32>,
}
impl Context {
    fn process(&mut self, values: &[String]) {
        self.output.extend(
            values.iter().map(|v| self.input.get(v).cloned().unwrap_or(0))
        ); // error: borrow of self.input conflicts with mutable borrow of self.output
    }
}

The closure captures self (because it uses self.input), which borrows all of self immutably. Meanwhile self.output.extend needs a mutable borrow of self.output. The borrow checker sees an immutable borrow of the whole self and a mutable borrow of a part of self and forbids it.

The fix is to capture only what the closure needs by introducing a local binding.

impl Context {
    fn process(&mut self, values: &[String]) {
        let input = &self.input;
        self.output.extend(
            values.iter().map(|v| input.get(v).cloned().unwrap_or(0))
        );
    }
}

Now the closure captures input, which is &self.input. The mutable borrow for self.output.extend and the immutable borrow for input refer to disjoint fields, so the compiler accepts it. This pattern—creating a local reference before the closure and letting the closure capture only that—is a precise way to control what a closure borrows.

RFC 2229 and precise capture:

An accepted RFC (available on nightly with #![feature(capture_disjoint_fields)]) changes closures so they capture individual fields instead of the whole struct. Once stable, many of these workarounds will become unnecessary.

Deciding Between Borrow and move

Choosing the right capture mode often comes down to two questions:

  • Does the closure need to live longer than the variables it uses? If yes, you must use move.
  • Do you need access to the variables after the closure is defined? If yes, borrowing (without move) is required, unless the type is Copy.

A common mistake is using move too eagerly for closures that will be called immediately and then dropped. That can make the original variable unusable when a simple borrow would have been enough. Reserve move for when the closure genuinely needs to take data with it beyond the current scope.

Another mistake is forgetting that move closures can still capture things by reference if you explicitly pass a reference. Writing move || ... captures variables by value, but if one of those variables is already a reference, the closure moves the reference itself—not the data behind it.

let data = vec![1, 2, 3];
let data_ref = &data;
let closure = move || println!("{:?}", data_ref);
closure();
println!("{:?}", data); // fine: only the reference was moved, not the vector

This can be useful when you need the closure to own the reference but not the underlying data, for example when the reference itself must be 'static but the data can stay on the original stack.

Borrow checker errors from capturing too much:

When a closure borrows a large struct just to read one field, and another operation needs a mutable reference to a different field, the compiler will complain. The local-binding workaround described earlier solves this. Recognizing this pattern will save you from unnecessary cloning or redesign.

Summary

Closures in Rust always capture their environment. Whether they borrow or take ownership is the central choice you make when writing them. The default borrows—immutable or mutable—keep the original data accessible and enforce the same aliasing rules that apply everywhere else in Rust. The move keyword hands ownership to the closure, freeing it from the stack frame that created it and making it possible to send closures across threads or return them from functions.

The key insight is that move is not a special “type of closure”; it is a directive about how variables enter the closure’s environment. The closure’s callability (Fn, FnMut, FnOnce) depends on what it does with those variables afterward. If you understand Rust’s ownership and borrowing rules, closures simply apply them to an anonymous struct that the compiler builds for you.

When you run into borrow checker issues with closures, ask yourself: is the closure capturing more than it needs? If so, narrow the capture with a local variable. Does the closure need to outlive the data? If so, use move. These two questions cover most closure-related errors you will encounter in real Rust code.