Closures

Closures are anonymous functions that can capture variables from around them — and in Rust, the ownership model makes "capture" a precise, three-way question: does the closure borrow, mutably borrow, or take ownership of what it captures? Understanding that is what makes closures (and the iterators that depend on them) click.

Closures are functions you write inline that can capture their surrounding environment, and they’re everywhere in idiomatic Rust — especially with iterators (the next post). But Rust’s ownership model makes closures more interesting than in most languages: how a closure captures a variable (by reference, by mutable reference, or by taking ownership) matters and is governed by the same ownership rules from Module 1. This post covers closure syntax, the three capture modes, the Fn traits, and why closures and ownership interact.

What a closure is

A closure is an anonymous function you can define inline and that can capture variables from its enclosing scope. The syntax uses |params| body:

let x = 10;
let add_x = |n| n + x;        // captures x from the environment
println!("{}", add_x(5));     // 15

// closures are often passed to other functions:
let mut nums = vec![3, 1, 2];
nums.sort_by(|a, b| a.cmp(b));   // an inline closure as the comparator

The defining feature — capturing the environment — is what distinguishes a closure from a plain function. add_x uses x from the surrounding scope without x being a parameter; the closure “closes over” x. This is why closures are so useful for short, inline behavior passed to other functions (sorting, filtering, mapping, callbacks) — they can reference the local context. Rust infers closure parameter and return types (unlike named functions, which require annotations), so closures are concise.

The three capture modes

Here’s where Rust’s ownership model makes closures distinctive. When a closure captures a variable, it does so in one of three ways, mirroring the ownership/borrowing rules from Module 1:

let s = String::from("hi");

let read = || println!("{}", s);          // borrows s immutably (&)
let mut owned_s = String::from("hey");
let mut modify = || owned_s.push('!');    // borrows owned_s mutably (&mut)

let take = move || println!("owns {}", s);  // `move` forces capture BY VALUE (ownership)

Rust infers the least-restrictive capture mode the closure needs (immutable borrow if it only reads, mutable if it modifies, by-value only if required). The move keyword forces capture by value — moving captured variables into the closure. This is essential when a closure must outlive the current scope or own its data — most importantly for spawning threads (a later concurrency topic): a thread’s closure must move its captures because the thread may run after the current scope ends, so it can’t borrow from it. So capture mode isn’t arbitrary; it follows directly from ownership — a closure that borrows can’t outlive what it borrows (lifetimes, Module 1), while a move closure owns its captures and can.

The Fn traits

Closures are represented by three traits, corresponding to the capture modes — and knowing them lets you accept closures as parameters:

These form a hierarchy (every Fn is also FnMut and FnOnce; every FnMut is FnOnce), reflecting that a closure needing less (just reading) can be used where more is allowed. You use these traits as bounds when a function accepts a closure:

fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
    f(x)                       // accepts any closure that reads its captures
}

fn apply_mut<F: FnMut()>(mut f: F) {
    f(); f();                  // accepts a closure that mutates captures, calls it twice
}

So closures connect back to traits (last posts): a function that takes a closure bounds it on Fn/FnMut/FnOnce (via generics for static dispatch, or Box<dyn Fn> for dynamic — the trait-object choice from the last post). The trait you require signals how the closure may use its captures. This is how the whole standard library accepts closures — map, filter, sort_by, thread spawning, and more all take Fn/FnMut/FnOnce-bounded parameters. (Closures are also zero-cost: a closure passed to a generic function is monomorphized and inlined like any generic, so closure-heavy code — including iterators — runs fast.)

Why closures and ownership interact

The deeper point: in many languages closures capture the environment loosely (by reference, garbage-collected), and you rarely think about how. In Rust, because there’s no garbage collector and ownership is explicit, how a closure captures is a real, checked question:

So closures are another place ownership shows up concretely: the capture mode determines what the closure can do (outlive, be returned, be threaded) and follows the same borrow/move rules as everything else. Understanding closures well means understanding their captures in ownership terms — which is exactly the discipline Module 1 built. This matters most for the next post (iterators, which use closures pervasively) and for concurrency (where move closures carry data into threads safely). The next post puts closures to work: iterators.

Key takeaways

Further reading

Sources & References

Closures and capture