Closures

A closure is an anonymous function you can store in a variable, pass around like any other value, and — unlike a plain function defined with fn — one that can reach into the scope where it was written and use variables from it. Closures are what make Rust’s iterator methods (map, filter, fold), callback-style APIs, and multi-threaded code so ergonomic: instead of writing a separate named function for every small piece of logic, you write it inline, right where it is needed. Under the hood a closure is not magic — the compiler turns each one into a small, unique, compiler-generated struct that stores exactly the data it captured and implements one of three special call traits. This lesson builds that mental model from the ground up, then works through capturing by reference, by mutable reference, and by value with move, closures as function parameters and return values, and the mistakes almost everyone runs into the first few times.

Overview: What a Closure Really Is

A function item defined with fn is completely self-contained: it cannot see any variable from the place where it happens to be called or defined, only its own parameters and whatever it declares locally. A closure is different. Write |x| x + offset next to a variable named offset, and the closure is allowed to use offset even though it is not one of its parameters — it “closes over” its environment, which is where the name comes from.

The interesting part is how a closure gets access to a captured variable. For every outside variable a closure body touches, the compiler looks at how the body actually uses that variable and automatically picks the least invasive option:

  • If the body only reads the variable, the closure captures it by immutable reference (&T).
  • If the body mutates the variable, the closure captures it by mutable reference (&mut T).
  • If the body needs to own the variable — moving it into something else, returning it, or the type only makes sense to own — the closure captures it by value (T), moving it out of the enclosing scope exactly like any other move.

This decision is made independently for each captured variable, so a single closure can borrow one variable immutably while moving another. Picture let x = 5; let show = || println!("{}", x);. The compiler sees the body only reads x, so it generates something conceptually like a small struct holding a reference to x with a method that runs the closure’s body — not literal code you would write, but a useful mental model for what the syntax expands into.

That generated struct implements one or more of three special traits, and which ones it implements determines where the closure can be used:

  • Fn — called through &self. Can be called any number of times, and only ever reads its captured data.
  • FnMut — called through &mut self. Can be called any number of times, and may mutate its captured data between calls.
  • FnOnce — called through self. May only be called once, because calling it can consume (move out of) its captured data.

Every closure implements at least FnOnce. If calling it doesn’t have to consume its captures, it also implements FnMut. If it additionally never mutates its captures, it also implements Fn. That gives a hierarchy: anywhere a FnOnce is accepted, an Fn or FnMut closure works too, because both promise strictly more than “callable once.”

The move keyword changes the default: writing move |...| ... forces every captured variable to be taken by value, even ones the body only reads. This matters when a closure needs to outlive the function it was created in — returned to a caller, or handed off to another thread with thread::spawn — because such a closure can no longer safely hold a reference into a local variable that is about to go out of scope. Owning the data instead makes the closure valid on its own, independent of where it was written.

One more piece of the mental model: every closure literal has its own unique, compiler-generated, unnameable type — even two closures with identical bodies are different types. That is why closures are almost always used behind a generic type parameter, like <F: Fn(i32) -> i32>, or an impl Trait return type, rather than by naming “the closure type” directly, and why Box<dyn Fn(i32) -> i32> shows up whenever closures of different shapes need to live in the same variable or collection.

Syntax

The general shape of a closure:

|param1: Type1, param2: Type2| -> ReturnType {
    // body — may use param1, param2,
    // and variables from the enclosing scope
}

// shorthand for a single-expression body:
let add_one = |x: i32| x + 1;
Part Meaning
|param1, param2| Parameter list between pipes. No parameters is written ||.
: Type Optional per-parameter type annotation. Usually omitted; the compiler infers it from how the closure is called.
-> ReturnType Optional explicit return type. Usually omitted and inferred from the body’s final expression.
{ ... } The body. Braces are optional when the body is a single expression, e.g. |x| x + 1.
move Optional keyword before the parameter list that forces every captured variable to be taken by value instead of by reference.

Examples

Example 1: A Basic Closure

The simplest closures take some parameters, do something with them, and either return a value from a single expression or run a block.

fn main() {
    let add = |a: i32, b: i32| a + b;
    let result = add(5, 7);
    println!("5 + 7 = {}", result);

    let greeting = |name: &str| {
        println!("Hello, {}!", name);
    };
    greeting("Rust");
}

Output:

5 + 7 = 12
Hello, Rust!

add has a single-expression body, so no braces, return, or semicolon are needed — the value of a + b is the closure’s result. greeting uses a block body instead, which lets it contain a full statement; note that even a block-bodied closure is assigned with a trailing semicolon on the let, and it still has to be called with greeting("Rust") just like any other value that implements a call trait.

Example 2: Capturing by Reference

When a closure only reads an outside variable, the compiler captures it by immutable reference, so the original variable is still fully usable afterward.

fn main() {
    let factor = 3;
    let multiply = |n: i32| n * factor;

    println!("4 * factor = {}", multiply(4));
    println!("factor is still usable: {}", factor);
}

Output:

4 * factor = 12
factor is still usable: 3

multiply reads factor but never changes or consumes it, so it captures &factor and implements Fn. Because it only borrowed factor, the variable is still available in main after the closure is done with it — no move happened.

Example 3: Capturing by Mutable Reference (FnMut)

When a closure body mutates a captured variable, the closure itself must be bound with mut, because calling it now requires a mutable reference to the closure’s own environment.

fn main() {
    let mut count = 0;
    let mut increment = || {
        count += 1;
        println!("count is now {}", count);
    };

    increment();
    increment();
    increment();
}

Output:

count is now 1
count is now 2
count is now 3

increment mutates count on every call, so the compiler captures it as &mut count and the closure implements FnMut. Since calling an FnMut closure needs &mut self, the increment binding itself has to be declared mut — that’s a very common first surprise, covered again in Common Mistakes.

Example 4: Capturing by Value With move

move is essential whenever a closure has to outlive the scope it was written in — the classic case is handing a closure to a new OS thread.

use std::thread;

fn main() {
    let data = vec![1, 2, 3, 4, 5];

    let handle = thread::spawn(move || {
        let sum: i32 = data.iter().sum();
        println!("Sum computed in thread: {}", sum);
    });

    handle.join().unwrap();
}

Output:

Sum computed in thread: 15

thread::spawn requires its closure to be 'static — it might run long after the current stack frame is gone — so it cannot hold a borrow into main‘s local data. The move keyword makes the closure take full ownership of data instead, so it owns everything it needs independent of main. handle.join() blocks the main thread until the spawned thread finishes and returns a Result that is Err only if the thread panicked; .unwrap() here simply propagates that panic into the main thread if it happens.

Example 5: Closures as Parameters and Return Values

Generic functions accept closures through a trait bound, and functions can hand back a closure using impl Trait instead of trying to name its type.

fn apply_twice<F>(f: F, x: i32) -> i32
where
    F: Fn(i32) -> i32,
{
    f(f(x))
}

fn make_adder(amount: i32) -> impl Fn(i32) -> i32 {
    move |x| x + amount
}

fn main() {
    let square = |x: i32| x * x;
    println!("apply_twice(square, 3) = {}", apply_twice(square, 3));

    let add_five = make_adder(5);
    println!("add_five(10) = {}", add_five(10));
}

Output:

apply_twice(square, 3) = 81
add_five(10) = 15

apply_twice is generic over any type F that implements Fn(i32) -> i32, so it accepts square directly and calls it twice: square(square(3)) is square(9) is 81. make_adder returns impl Fn(i32) -> i32 — some concrete closure type the caller never needs to name — and the closure is marked move because amount is a parameter local to make_adder; without move the closure would try to borrow a value that is destroyed the moment make_adder returns, which the borrow checker would reject.

How It Works Step by Step

Tracing Example 3, the FnMut counter:

  • The compiler scans the closure body { count += 1; println!(...) } and sees count is mutated, so it decides to capture count by mutable reference.
  • It generates an anonymous struct roughly equivalent to struct Counter<'a> { count: &'a mut i32 } and implements FnMut() for it — the call operator takes &mut self, dereferences the field, and increments it.
  • Because calling that generated struct requires &mut self, the variable increment holding it must itself be declared mut — mutability of the closure’s storage is what lets Rust call through to the mutable capture.
  • Each of the three increment() calls borrows increment mutably for the duration of that single call, runs the body, and releases the borrow immediately after, which is why three sequential calls are allowed even though each one technically needs exclusive access.

Tracing Example 4, the move closure sent to a thread:

  • data is created in main‘s stack frame as a Vec<i32>, which owns its heap buffer.
  • The closure passed to thread::spawn uses move, so data is moved into the closure’s generated struct — after this line, data no longer exists as a usable name in main.
  • Because the closure now owns everything it touches, it satisfies the 'static bound thread::spawn requires — nothing about it depends on main‘s stack still existing.
  • The new thread runs the closure body, computing sum and printing it; meanwhile main is blocked at handle.join() until that thread finishes, at which point join() returns and .unwrap() extracts the (here, unused) success value.

Common Mistakes

1. Using a Value After It Has Been Moved Into a move Closure

Adding move unconditionally moves every captured variable, even non-Copy ones you might still want to use afterward:

fn main() {
    let name = String::from("Ferris");
    let print_name = move || println!("Hello, {}", name);

    print_name();
    println!("{}", name);
}

This fails to compile with a “borrow of moved value: name” error. String is not Copy, and move forced it into print_name‘s environment, so the binding name in main is no longer valid by the time the second println! tries to read it. The fix is usually to drop move if the closure doesn’t need to outlive the current scope, letting it borrow instead:

fn main() {
    let name = String::from("Ferris");
    let print_name = || println!("Hello, {}", name);

    print_name();
    println!("{}", name);
}

Output:

Hello, Ferris
Ferris

Without move, the closure only needs a shared reference to format name, so name is still owned by main and usable right after.

2. Forgetting mut on a Closure That Mutates Its Captures

fn main() {
    let mut count = 0;
    let increment = || {
        count += 1;
        println!("count is now {}", count);
    };
    increment();
}

This is rejected with “cannot borrow increment as mutable, as it is not declared as mutable.” The closure body mutates count, so the generated struct implements FnMut, and calling anything through FnMut requires &mut self — which means the variable holding the closure must be mut, exactly like any other value you intend to mutate through. The fix is a one-word change:

let mut count = 0;
let mut increment = || {
    count += 1;
    println!("count is now {}", count);
};
increment();

Output:

count is now 1

3. An Active Mutable Borrow Blocking a Later Immutable Use

fn main() {
    let mut vec = vec![1, 2, 3];
    let mut push_value = || vec.push(4);
    println!("{:?}", vec);
    push_value();
}

This looks harmless — the println! comes before push_value() is even called — but it fails with “cannot borrow vec as immutable because it is also borrowed as mutable.” push_value captures vec by mutable reference, and that borrow has to stay valid for as long as push_value might still be used, which includes the call at the bottom of the function. The borrow checker sees the mutable borrow as alive across the whole function, which conflicts with the immutable borrow the println! needs. Reordering so the closure’s last use comes before the conflicting read fixes it:

fn main() {
    let mut vec = vec![1, 2, 3];
    let mut push_value = || vec.push(4);
    push_value();
    println!("{:?}", vec);
}

Output:

[1, 2, 3, 4]

Now the mutable borrow held by push_value ends right after push_value() runs, so the println!‘s immutable borrow of vec is free to start.

Best Practices

  • Let the compiler infer the capture mode by default; only add move when the closure genuinely needs to outlive the current scope, such as being returned or sent to another thread.
  • Accept the loosest trait bound your function actually needs: Fn if you only call it and read captures, FnMut if you need to mutate through it, FnOnce if it’s only ever called at most once (common for consuming builders).
  • Prefer impl Fn(...) -> ... when returning a single closure; reach for Box<dyn Fn...> only when closures of different shapes need to be stored together, such as in a Vec.
  • Keep closures short. If one grows past a couple of lines or needs its own unit tests, promote it to a named function instead.
  • Before reaching for Rc, RefCell, or a defensive .clone() to silence a borrow-checker error inside a closure, check whether simply reordering statements or shrinking the closure’s live range fixes it, as in the mutable-borrow example above.
  • Remember every closure literal is its own anonymous type, even when two closures look identical — that’s why they’re passed through generics or impl Trait rather than a named type.
  • Only call .unwrap() on a JoinHandle::join() result when a panicking worker thread should also panic the caller; otherwise match on the Result to handle the failure gracefully.

Practice Exercises

  • Write a closure named word_count that takes a &str and returns the number of whitespace-separated words in it as a usize (hint: .split_whitespace().count()). Call it on "the quick brown fox" and print the result. Expected output: 4.
  • Write a function for_each_doubled(numbers: &[i32], mut f: impl FnMut(i32)) that calls f once per element of numbers, passing each element doubled. In main, call it on [1, 2, 3, 4] with a closure that adds each value into a running mut total, then print the total. Expected output: 20.
  • Write make_multiplier(factor: i32) -> impl Fn(i32) -> i32, modeled on make_adder from Example 5 but multiplying instead of adding. Call make_multiplier(3) and apply the result to 7. Expected output: 21.

Summary

  • A closure is an anonymous, callable value that can capture variables from its surrounding scope, unlike a plain fn.
  • The compiler infers the tightest capture mode per variable: immutable reference, mutable reference, or move — based on how the closure body uses it.
  • Three traits describe what a closure can do: Fn (read-only, callable many times), FnMut (may mutate captures, callable many times), and FnOnce (may consume captures, callable once). Every closure implements at least FnOnce.
  • The move keyword forces every capture to be taken by value; use it when a closure must outlive its defining scope, such as being returned or sent to a thread.
  • A closure that mutates its captures must itself be bound with mut, because calling it needs &mut self.
  • Each closure literal has its own unique, unnameable type — pass closures through generic trait bounds or impl Trait, and use Box<dyn Fn...> only when different closure shapes must share a variable.
  • Borrow-checker errors involving closures usually come down to a capture’s borrow staying alive longer than expected — check where the closure is last used, not just where it’s defined.