What Are Closures in Rust and How Do They Work?

Closures are anonymous functions in Rust that can capture and use variables from their defining scope.

When a function needs context

You're building a simple event loop. A button click needs to increment a counter, but the counter lives in the main function. You pass a function to the button, and that function needs to reach back and grab the counter. In JavaScript, you'd write an arrow function and it works. In Rust, you write a closure, and the compiler immediately starts asking questions about who owns that counter and whether you're allowed to change it.

Closures solve the problem of passing behavior that depends on local state. They are anonymous functions that capture variables from their surrounding environment. You define them with the |args| body syntax. You can assign them to variables, pass them to other functions, or store them in structs. The compiler generates a unique type for each closure, implementing traits that describe how the closure interacts with its captured data.

The backpack analogy

A normal function is a tool. You pick it up, use it, and put it down. It has no memory of where it came from. A closure is a tool with a backpack. The backpack contains variables from the place where the closure was created. When the closure runs, it can use those variables.

The backpack has rules. If the closure only reads a variable, the backpack holds a reference. If the closure modifies a variable, the backpack holds a mutable reference. If the closure needs to take ownership, the backpack holds the value itself. The compiler decides which rule applies based on what you do inside the closure body. You can also force the backpack to take ownership using the move keyword.

Minimal example

fn main() {
    let multiplier = 10;
    // Closure syntax: |args| body.
    // Captures `multiplier` by reference because it only reads the value.
    let multiply = |x| x * multiplier;
    
    // Calling the closure works like a function call.
    println!("{}", multiply(5)); // 50
    println!("{}", multiply(7)); // 70
}

The closure multiply captures multiplier by reference. The compiler infers this because the body only reads multiplier. Since i32 implements Copy, the compiler could copy the value, but for non-Copy types, it would borrow. The closure can be called multiple times because borrowing doesn't consume the variable.

Closures are data. Treat them as structs, and the type system makes sense.

Under the hood: Structs and traits

The compiler generates a unique, unnameable struct for every closure. This struct holds the captured variables as fields. When you call the closure, you're invoking a method on that struct. The method signature depends on how the closure captures its environment.

// Conceptual view of what the compiler generates.
// You cannot write this code manually.

struct ClosureImpl<'a> {
    multiplier: &'a i32,
}

impl<'a> FnOnce<(i32,)> for ClosureImpl<'a> {
    type Output = i32;
    extern "rust-call" fn call_once(self, args: (i32,)) -> Self::Output {
        args.0 * self.multiplier
    }
}

The closure implements one or more of the Fn family traits. These traits define whether the closure can be called multiple times and whether it mutates its captures. The compiler automatically implements these traits based on the closure body. You rarely write these trait implementations yourself, but understanding them explains why the compiler rejects certain closure usages.

The three capture modes

Closures implement traits that correspond to three capture modes. The compiler picks the most permissive mode that satisfies the body.

Fn: Read-only capture

A closure implements Fn when it captures variables by reference and never mutates them. You can call this closure multiple times. It's the safest mode.

fn main() {
    let config = String::from("debug");
    // Captures `config` by reference. Implements `Fn`.
    let logger = |msg| println!("[{}] {}", config, msg);
    
    logger("Starting");
    logger("Done");
    // `config` is still usable here because it was borrowed.
    println!("Config: {}", config);
}

FnMut: Mutable capture

A closure implements FnMut when it captures variables by mutable reference. It can modify the captured state. You can call it multiple times, but you need a mutable reference to the closure itself.

fn main() {
    let mut counter = 0;
    // Captures `counter` mutably. Implements `FnMut`.
    let mut increment = || counter += 1;
    
    increment();
    increment();
    println!("{}", counter); // 2
}

The variable increment must be declared mut because calling a FnMut closure requires a mutable borrow of the closure. This reflects the fact that the closure modifies its internal state.

FnOnce: Consuming capture

A closure implements FnOnce when it captures variables by move. It consumes the captured data. You can call it exactly once. After the call, the captures are gone.

fn main() {
    let data = String::from("secret");
    // Captures `data` by move. Implements `FnOnce`.
    let consume = || {
        println!("{}", data);
        // `data` is moved into the closure.
    };
    
    consume();
    // Error: `data` was moved into the closure.
    // println!("{}", data);
}

This closure takes ownership of data. The compiler infers FnOnce because String doesn't implement Copy, and the closure doesn't explicitly borrow. If you try to call consume again, the compiler rejects it with E0512 (cannot apply unary operator * to type fn() that does not dereference), or more commonly, you get a trait bound error because the closure is no longer callable.

Match the trait to the behavior. Fn for read-only, FnMut for mutation, FnOnce for consumption. The compiler enforces the contract.

The move keyword

By default, the compiler captures variables by reference if possible. Sometimes you need to force the closure to take ownership. Use the move keyword before the arguments. This forces all captures to be moved into the closure, regardless of what the body does.

fn main() {
    let data = vec![1, 2, 3];
    // `move` forces ownership transfer of `data`.
    let closure = move || {
        println!("{:?}", data);
    };
    
    // `data` is moved. Cannot use here.
    // println!("{:?}", data);
    
    closure();
}

The move keyword is essential when sending closures to other threads. Threads may outlive the current scope, so the closure must own its data. It's also useful when storing a closure in a struct that outlives the current function.

Use move to force ownership. The compiler won't infer it for you, so be explicit when the closure must outlive the scope.

Storing closures and type inference

Closures have unique types. You cannot declare a variable with a closure type directly. You use impl Trait or trait objects.

fn main() {
    let x = 5;
    // Type is unique and unnameable.
    let c = || x + 1;
    
    // Store using `impl Fn`.
    let stored: impl Fn() -> i32 = || 42;
    println!("{}", stored());
}

If you need to store a closure in a struct field, you usually need a trait object like Box<dyn Fn()>. This adds a layer of indirection and dynamic dispatch.

struct Handler {
    // Trait object allows storing any closure matching the signature.
    callback: Box<dyn Fn(i32) -> String>,
}

impl Handler {
    fn new<F>(f: F) -> Self
    where
        F: Fn(i32) -> String + 'static,
    {
        Handler {
            callback: Box::new(f),
        }
    }
}

The 'static bound ensures the closure doesn't borrow data that might be dropped. This is a common requirement for stored callbacks.

Closures infer their argument types from usage. Once inferred, the types are fixed. You cannot call a closure with different types later.

fn main() {
    let add = |x, y| x + y;
    add(1, 2); // Infers i32.
    // add(1.0, 2.0); // Error: type mismatch.
}

If you need a closure that works with multiple types, you must annotate the arguments explicitly.

fn main() {
    let add = |x: i32, y: i32| x + y;
    // Still fixed to i32.
    // For generics, use a function or a trait implementation.
}

Convention aside: When naming closure variables, use action-oriented names like handle_click, filter_fn, or transform. This signals that the variable holds behavior, not just data. Also, prefer Rc::clone(&data) over data.clone() when cloning reference-counted pointers. The explicit form makes it clear you're incrementing the reference count, not deep-copying the value.

Pitfalls and compiler errors

Closures interact with the borrow checker in subtle ways. Here are common errors.

E0382: Use of moved value

If a closure captures a variable by move, you cannot use that variable after the closure is created.

fn main() {
    let data = String::from("hello");
    let closure = move || println!("{}", data);
    // Error: E0382. `data` was moved into the closure.
    println!("{}", data);
}

Fix this by cloning the data before capturing, or by removing move if borrowing is sufficient.

E0502: Borrow conflict

You cannot borrow a variable mutably inside a closure while an immutable borrow exists.

fn main() {
    let mut data = vec![1, 2, 3];
    let immutable_ref = &data;
    // Error: E0502. Cannot borrow `data` as mutable because it is also borrowed as immutable.
    let mut closure = || data.push(4);
    closure();
    println!("{:?}", immutable_ref);
}

The closure captures data mutably. This conflicts with immutable_ref. Drop the immutable borrow before creating the closure, or restructure the code to avoid the overlap.

E0277: Trait bound not satisfied

Functions that accept closures often require specific traits. If your closure doesn't satisfy the trait, the compiler rejects it.

fn call_twice<F>(f: F)
where
    F: Fn(),
{
    f();
    f();
}

fn main() {
    let mut count = 0;
    // This closure modifies `count`, so it's `FnMut`.
    let increment = || count += 1;
    // Error: E0277. `increment` is `FnMut`, not `Fn`.
    call_twice(increment);
}

The function call_twice requires Fn, but increment is FnMut. Change the function signature to accept FnMut, or rewrite the closure to avoid mutation.

Read the trait bound error. The compiler is telling you exactly how your closure interacts with its environment. Fix the capture mode, and the code compiles.

Decision matrix

Use closures for inline logic when you need to pass behavior to a function like map, filter, or a callback handler. Use named functions when the logic is complex, reused in multiple places, or doesn't need to capture local variables. Use the move keyword when you spawn a thread or store the closure in a struct that outlives the current scope, forcing the closure to take ownership of its captures. Use Fn trait bounds when your API accepts a closure that reads captured data but never changes it. Use FnMut trait bounds when the closure needs to modify captured variables, like incrementing a counter. Use FnOnce trait bounds when the closure consumes its captures or when you only intend to call it a single time.

Pick the tool that matches the lifetime and mutation needs. The borrow checker will enforce the rest.

Where to go next