Closures

An introduction to closures in Rust - anonymous functions that capture their environment, how they differ from named functions, and when to use them.

If you have written a function that only gets used once, or you need to hand a small piece of behavior to a method like map or unwrap_or_else, Rust offers a tool lighter than a full fn definition: closures. They are the mechanism that lets you write an inline, anonymous function that can also reach out and use variables from the surrounding code.

Closures at a Glance:

A closure is an anonymous function that can capture values from its enclosing scope. Functions defined with fn cannot do this — they only see their parameters and any items in scope that they explicitly reference.

What a Closure Is

A closure in Rust is a value that behaves like a function but is defined without a required name. Syntactically, you write it with a pair of vertical pipes containing the parameters, followed by the body.

let say_hello = || println!("Hello!");
say_hello(); // prints "Hello!"

This example stores a closure in the variable say_hello and calls it later. The closure takes no parameters (||) and runs println!. The crucial part, and what separates closures from plain functions, is that they can close over variables that exist at the point they are created.

In the snippet below, the closure captures prefix from the outer scope and uses it every time it runs:

let prefix = "Item: ";
let label = |name: &str| format!("{}{}", prefix, name);
println!("{}", label("widget")); // Item: widget

No additional parameter plumbing is needed. The closure just grabs what it needs from the environment.

Why Closures Exist in Rust

Two main motivations drive the inclusion of closures in the language:

  1. Inline behavior for higher-order functions. Iterators, Option combinators, and sorting methods all accept closures to customize their logic without forcing you to define a separate named function elsewhere.
  2. Environment capture. When a method like unwrap_or_else needs a fallback value that depends on local state, a closure can borrow that state and produce the value on demand. A function pointer cannot carry local variables along with it.

The standard library is designed with this in mind. Many APIs expect a closure precisely because they need to defer execution while holding on to contextual data.

Closures vs. Functions

A function definition always looks like this:

fn add_one(x: u32) -> u32 {
    x + 1
}

A closure that does the same job can be written with far less ceremony:

let add_one = |x: u32| -> u32 { x + 1 };
let add_one_inferred = |x| x + 1;

The differences go beyond syntax.

PropertyFunctions (fn)Closures
NameRequiredOptional (often anonymous)
Type annotationsRequired for parameters and return typeOften inferred by the compiler
Environment captureNot possibleImmutable borrow, mutable borrow, or ownership
Where definedAt item levelAnywhere an expression is allowed

Compiler Inference:

The compiler’s ability to infer closure parameter and return types often removes the need for boilerplate. This is one of the reasons closures feel so lightweight compared to explicit function pointers.

Closure Syntax and Type Inference

Closures can be written in progressively shorter forms, each compiling to the same behavior:

// Fully annotated
let f1 = |x: i32| -> i32 { x + 1 };
// Parameters annotated, return type inferred
let f2 = |x: i32| { x + 1 };
// No annotations when a single expression body allows brace elision
let f3 = |x| { x + 1 };
let f4 = |x| x + 1;

The compiler determines the concrete types the first time the closure is called. Once set, those types are locked in. Calling the same closure with a different type later will fail.

let identity = |x| x;
let s = identity(String::from("hello"));
// let n = identity(5);  // ERROR: expected String, found integer

Type Locking:

The first call to a closure fixes its parameter and return types. Attempting to reuse the same closure variable with a different type results in a mismatched types compile error. If you genuinely need the same logic over multiple types, use a generic function or a trait bound instead.

Capturing the Environment

When a closure references a variable from an outer scope, Rust automatically determines how that variable should be captured. It chooses among three modes, mirroring the ways a function can take an argument:

  • Immutable borrow (&T) — the closure only reads the value.
  • Mutable borrow (&mut T) — the closure modifies the value.
  • Ownership (T) — the closure takes full ownership, often triggered by the move keyword or by the closure body requiring it.

This decision is driven by what the closure body actually does with the captured variable.

let mut items = vec![1, 2, 3];
// Immutable borrow: the closure only prints the list
let printer = || println!("{:?}", items);
printer(); // items is still usable afterwards
// Mutable borrow: the closure pushes a new element
let mut pusher = || items.push(4);
pusher(); // items is now [1, 2, 3, 4]
// items cannot be used until pusher is dropped, due to the mutable borrow

The borrow checker enforces the usual rules: you cannot have a mutable borrow and an immutable borrow active at the same time, and captured references must remain valid for the closure’s lifetime.

Move Closures

The move keyword forces a closure to take ownership of the variables it uses, even if the body doesn't strictly require it. This is essential when the closure needs to outlive the current scope — for example, when spawning a thread or returning a closure from a function.

let message = String::from("hello");
let printer = move || println!("{}", message);
// println!("{}", message);  // ERROR: value moved into the closure
printer();

After a move closure takes message, the original binding is no longer usable. This prevents dangling references when the outer scope is destroyed but the closure lives on elsewhere.

Use‑After‑Move:

Once a move closure has captured a value by ownership, the original variable is unusable. Trying to access it after the closure definition will result in a "use of moved value" compiler error. This is the same ownership rule that applies to any moved value, but it can be surprising when the capture happens implicitly at the closure boundary.

Closure Trait Types

Every closure implements one or more of three traits from the standard library: Fn, FnMut, and FnOnce. Which trait gets implemented depends on how the closure uses its captured variables.

  • FnOnce — can be called at least once. All closures implement this. A closure that consumes a captured value (e.g., dropping it or moving it out) implements only FnOnce.
  • FnMut — can be called multiple times and may mutate captured values. Requires &mut self.
  • Fn — can be called multiple times without mutating captured state. Requires &self.
let name = String::from("Rust");
let consume_name = || drop(name);       // FnOnce only
let mut count = 0;
let mut increment = || count += 1;      // FnMut (and FnOnce)
let print_name = || println!("Rust");   // Fn (and FnMut, FnOnce)

These trait distinctions are what allow Rust to reason about borrowing and mutation through closures, and they surface in places where you see generic bounds like F: FnOnce().

Closures and Safety

Because closures capture references and can outlive the scope that created them, the borrow checker must guarantee that all captures remain valid. A closure that borrows items immutably cannot live longer than items itself. If you try to return such a closure from a function, the compiler will reject the code unless the captured reference’s lifetime is clear.

The interplay between closures and ownership prevents entire categories of use-after-free bugs. If a closure owns its data (via move), it can safely move to another thread or be stored indefinitely. If it only borrows, its lifetime is tied to the borrowed data.

Capturing Across Scopes:

Returning a closure that borrows local variables almost always fails to compile. When you need a closure to leave the current function, use move to give it ownership of the required data, or restructure to avoid the borrow.

Using Closures in Practice

Closures appear most frequently with iterator adapters and combinator methods:

let numbers = vec![1, 2, 3, 4, 5];
let even_squares: Vec<_> = numbers
    .iter()
    .filter(|&&n| n % 2 == 0)
    .map(|&n| n * n)
    .collect();

They also serve as the primary argument to methods like unwrap_or_else, unwrap_or_default, sort_by, and binary_search_by. In all these cases, the closure provides a small piece of logic that operates on data owned by the method, without requiring a named function.

For callbacks and event-driven code, closures capture the necessary context without introducing extra struct fields or global state. Combined with move, they are the standard way to pass behaviour into threads and asynchronous tasks.

The companion pages in this chapter dive deeper into each of these patterns, covering capturing rules, move semantics, trait resolution, thread safety, and performance.

Performance

Rust’s closures are a zero‑cost abstraction. The compiler monomorphizes each closure usage just like it does with generic functions, generating specialized code for each concrete combination of closure and captures. The resulting machine code is typically identical to what you would write manually with a struct holding the captured variables and a method implementing the body.

Unlike languages where closures always imply heap allocation or dynamic dispatch, Rust closures are stack‑allocated by default. Only when you explicitly box them (e.g., Box<dyn FnOnce()>) does heap allocation enter the picture.

Zero-Cost Iteration:

When you write numbers.iter().map(|x| x * 2).collect(), the generated assembly is comparable to a hand‑written for loop. The closure incurs no function call overhead after optimization.