How to use the move keyword with closures

Use the move keyword in a closure definition to force ownership transfer of captured variables.

The closure that steals your data

You have a String in your function. You want to pass a closure to a thread so the thread can print that string. You write the closure. The compiler rejects the code with a lifetime error. The closure tries to borrow the string, but the thread might run after your function returns. The borrow would dangle.

You add the move keyword before the closure body. The error vanishes. The thread prints the message. Your function can no longer touch the string. The closure took it.

That is the move keyword. It forces a closure to take ownership of captured variables instead of borrowing them. It changes the closure from a temporary borrower into a permanent owner. You use it when the closure needs to outlive the current scope, or when you want to transfer a resource to the closure and stop using it yourself.

How closures capture by default

Closures are anonymous functions that can access variables from the surrounding scope. Rust calls this capturing. By default, closures are lazy. They borrow variables. They take references. This is efficient. It avoids copying data. It keeps the original owner happy.

The compiler analyzes the closure body. It finds every variable used inside. It adds a reference to each variable in the closure's environment. The closure type becomes something like Fn(&T1, &T2, ...). The original variables remain usable. The closure is just peeking at them.

This default works for most cases. The closure runs quickly. The data stays alive. The borrow checker ensures the references are valid. But the default breaks when the closure needs to live longer than the variables. Threads are the classic offender. Async tasks are another. Returning a closure from a function is a third. In these cases, borrowing is impossible. The closure must own the data.

The move keyword takes ownership

The move keyword changes the capture strategy. It tells the compiler to move values into the closure's environment instead of borrowing them. The closure captures by value. The original variables are invalidated. The closure owns the data.

The closure type changes. Instead of holding references, it holds the values. The type becomes something like Fn(T1, T2, ...). The closure can now be sent to another thread. It can be returned from a function. It can outlive the scope where it was created. The data travels with it.

The trade-off is ownership. Once you move a value, you cannot use it in the original scope. The compiler enforces this. If you try to use a moved variable, you get a compile error. The closure has the data. You do not.

Minimal example

fn main() {
    // Create a heap-allocated string.
    let s = String::from("hello");

    // The `move` keyword forces the closure to take ownership of `s`.
    // Without `move`, the closure would borrow `s` by default.
    let closure = move || {
        // `s` is now owned by the closure's environment.
        // The closure can use `s` even if `main` ends.
        println!("{s}");
    };

    // Calling the closure executes the code.
    closure();

    // This line would cause a compile error: E0382 (use of moved value).
    // `s` has been moved into the closure. It is no longer valid here.
    // println!("{s}");
}

The closure captures s. The move keyword ensures s is moved into the closure. The variable s in main is gone. The closure holds the string. When the closure runs, it prints the string. If you uncomment the last line, the compiler stops you. The data is in one place. Ownership is clear.

What happens under the hood

Closures are implemented as anonymous structs. Each captured variable becomes a field in the struct. The move keyword determines how those fields are populated.

Without move, the fields are references. The struct holds &T. The compiler inserts borrows. The struct is small. It points back to the original data.

With move, the fields are values. The struct holds T. The compiler inserts moves. The struct owns the data. The struct might be larger. It contains the data itself.

The closure also implements a trait. Fn, FnMut, or FnOnce. This depends on how the closure uses the captured data, not on move. move affects capture. The trait affects callability. A move closure can still be Fn if it only reads the data. It becomes FnOnce if it consumes the data.

fn main() {
    let s = String::from("hello");

    // This closure moves `s` but only reads it.
    // It implements `Fn`. It can be called multiple times.
    let read_closure = move || {
        println!("{s}");
    };

    read_closure();
    read_closure();

    // This closure moves `s` and consumes it.
    // It implements `FnOnce`. It can be called only once.
    let consume_closure = move || {
        drop(s);
    };

    consume_closure();
    // consume_closure(); // Error: closure cannot be called more than once
}

The first closure moves s but calls println!, which borrows s. The closure retains ownership and can be called again. The second closure moves s and calls drop, which consumes s. The closure is exhausted after one call. move does not make a closure FnOnce. Consuming the data does.

Real-world: Threads and async

Threads are the most common reason to use move. A thread runs independently. It might outlive the calling function. The closure passed to the thread must own its data.

use std::thread;

fn main() {
    // Create data that the thread will use.
    let data = vec![1, 2, 3];

    // Spawn a thread that owns `data`.
    // `thread::spawn` takes ownership of the closure.
    // The closure must own its captures to be `Send`.
    let handle = thread::spawn(move || {
        // The thread has its own copy of `data` (moved in).
        // It can use `data` safely even if `main` returns.
        for item in &data {
            println!("Thread sees: {item}");
        }
    });

    // `data` is moved into the thread.
    // We cannot use `data` here.
    // println!("{data}"); // Error: value borrowed here after move

    // Wait for the thread to finish.
    handle.join().unwrap();
}

The move keyword is essential here. Without it, the closure would borrow data. The borrow would be invalid if main returns before the thread finishes. The compiler rejects this with E0373: "closure may outlive the current function, but it borrows data which is owned by the current function". Adding move fixes the error. The thread owns the vector. The data lives as long as the thread.

Async code uses the same pattern. Async executors spawn tasks that run on different threads. Tasks must own their data. tokio::spawn and async_std::task::spawn require move closures. The convention is to add move to every closure passed to a spawner. It signals that the task is independent.

Pitfalls and compiler errors

The move keyword has quirks. It interacts with Copy types, mutability, and partial moves.

Move copies Copy types

If a captured type implements Copy, move copies the value. The original variable remains valid. This is because copying is cheap and safe. The compiler treats Copy types specially.

fn main() {
    let x = 5;

    // `i32` implements `Copy`.
    // `move` copies `x` into the closure.
    // `x` is still usable in `main`.
    let closure = move || {
        println!("{x}");
    };

    closure();
    println!("{x}"); // OK: `x` was copied, not moved.
}

This can be confusing. You add move expecting to invalidate the variable. The variable stays alive. The type is Copy. The compiler made a copy. Check the trait implementation. If the type is Copy, move is a copy.

Move captures everything used

The move keyword applies to all captured variables. If the closure uses multiple variables, they are all moved. This can cause errors if you only wanted to move one.

fn main() {
    let s = String::from("hello");
    let n = 42;

    // The closure uses both `s` and `n`.
    // `move` moves both variables.
    let closure = move || {
        println!("{s} {n}");
    };

    closure();

    // `s` is moved. Error: E0382.
    // `n` is copied. OK.
    // println!("{s}");
    println!("{n}");
}

The closure moves s and copies n. s is invalid. n is valid. If you need to use s after the closure, you must clone it before moving.

fn main() {
    let s = String::from("hello");
    let s_clone = s.clone();

    // Move the clone, keep the original.
    let closure = move || {
        println!("{s_clone}");
    };

    closure();
    println!("{s}"); // OK: original is still valid.
}

Cloning before moving is a common pattern. It lets you share data between the closure and the original scope. The cost is an allocation. Use it when you need both.

Move and mutability

Moving a value does not make it mutable. The closure still respects mutability rules. If you move an immutable variable, the closure cannot mutate it.

fn main() {
    let s = String::from("hello");

    // `s` is immutable.
    // The closure moves `s`, but `s` is still immutable inside.
    let closure = move || {
        // Error: cannot assign to `s`, as it is a captured variable in a `Fn` closure
        // s.push_str(" world");
    };
}

To mutate moved data, you need interior mutability. Wrap the data in RefCell or Mutex. Move the wrapper. Mutate through the wrapper.

use std::cell::RefCell;

fn main() {
    // Wrap the string in a RefCell for interior mutability.
    let data = RefCell::new(String::from("hello"));

    // Move the RefCell into the closure.
    let closure = move || {
        // Borrow mutably through the RefCell.
        data.borrow_mut().push_str(" world");
    };

    closure();
    println!("{:?}", data.borrow());
}

The RefCell is moved. The closure owns the RefCell. The closure can mutate the string inside. This pattern is common in UI code and state machines. The closure owns the state. It mutates the state safely.

Error E0507: Cannot move out of borrowed content

If you try to move a field of a reference, the compiler rejects it. You cannot move data out of a borrowed value.

fn main() {
    let s = String::from("hello");
    let r = &s;

    // Error: E0507 (cannot move out of borrowed content).
    // `r` is a reference. You cannot move `s` out of it.
    // let closure = move || println!("{r}");
}

The closure captures r. r is a reference. The closure cannot move the underlying string. It can only borrow it. If you need to move the string, capture the string, not the reference.

Decision: move or borrow

Choose the capture strategy based on lifetime and ownership needs.

Use move when passing a closure to a thread or async executor. The thread runs independently and might outlive the calling scope. The closure must own its data. The data must be Send.

Use move when returning a closure from a function. The returned closure might be called after the function returns. It cannot borrow local variables. It must move them. The closure owns the data.

Use move when you want the closure to consume a resource. The caller should not use the resource anymore. The closure takes ownership. The resource is transferred.

Use a standard closure without move when the closure borrows data that has a longer lifetime than the closure. This avoids unnecessary allocations. It keeps the original owner valid. The closure is just a view.

Use move with Copy types only for documentation. The compiler copies the value automatically. Adding move makes the intent clear but changes nothing at runtime. It signals that the closure uses the value, not a reference.

Use RefCell or Mutex with move when you need to mutate captured data. The closure owns the wrapper. It mutates the data through interior mutability. This is safe and idiomatic.

The closure owns the data. The original scope loses it. That is the deal. Pick the strategy that matches your lifetime requirements.

Where to go next