Capturing the Environment with Closures
Understand how Rust closures capture variables from their defining scope, the three capture modes, and how this relates to the Fn, FnMut, and FnOnce traits.
Capturing the Environment with Closures
A closure in Rust can reach outside its own body and use variables that are defined in the surrounding scope. This ability is called capturing the environment. Functions, in contrast, cannot do this—they can only operate on data passed in as arguments. That one difference is what makes closures so useful for inline, context-aware operations.
If you come from languages where all functions can access outer variables freely, the idea itself is familiar. What makes Rust closures special is that the compiler does not just grab a reference blindly. It decides, based on how you use the captured variables inside the closure body, whether to borrow immutably, borrow mutably, or take ownership.
A Simple Example of Capture
The closure add_x borrows the variable x from its environment without having x passed as a parameter.
fn main() {
let x = 4;
let add_x = |y| y + x; // captures x by immutable reference
println!("{}", add_x(3)); // prints 7
println!("{}", x); // x is still usable
}
The line let add_x = |y| y + x; does three things:
- It creates an anonymous function that takes one parameter
y. - It captures
xfrom the outer scope. - Because
xis only read, the closure borrowsximmutably.
After the closure is defined and called, x is still available—the borrow ended when the closure was last used. If you tried to write this as a regular function, you would have to pass x explicitly.
Borrowing that works as expected:
When a closure only reads a variable, you can keep using that variable before, between, and after calls to the closure. The borrow checker treats it like any other immutable borrow.
Why Closures Need to Capture
The practical motivation is deferred computation with context. A common pattern is supplying a fallback value only when needed. The standard library method Option::unwrap_or_else expects a closure that produces a fallback. That closure must be able to see the surrounding state—otherwise it could not compute a meaningful value.
Consider a T‑shirt inventory that gives away shirts based on user preference or, if no preference is given, chooses the colour that is most stocked:
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
Red,
Blue,
}
struct Inventory {
shirts: Vec<ShirtColor>,
}
impl Inventory {
fn most_stocked(&self) -> ShirtColor {
let mut num_red = 0;
let mut num_blue = 0;
for color in &self.shirts {
match color {
ShirtColor::Red => num_red += 1,
ShirtColor::Blue => num_blue += 1,
}
}
if num_red > num_blue { ShirtColor::Red } else { ShirtColor::Blue }
}
fn giveaway(&self, preference: Option<ShirtColor>) -> ShirtColor {
preference.unwrap_or_else(|| self.most_stocked())
}
}
The closure || self.most_stocked() captures self—the Inventory instance—by immutable reference. It doesn’t receive self as a parameter; the compiler wires that reference into the closure automatically. unwrap_or_else calls the closure only when preference is None, and the closure can read the inventory because it carries the reference.
This pattern would be impossible with a plain function: a function cannot “remember” a reference to self unless it is passed explicitly, and unwrap_or_else doesn’t give you a way to thread extra arguments through.
How the Compiler Chooses a Capture Mode
Rust determines the capture mode per variable by examining the closure body. There are three possibilities, which correspond directly to the three ways you can pass a parameter to a function:
- Immutable reference – when the variable is only read.
- Mutable reference – when the variable is mutated.
- Ownership – when the variable is moved into the closure (for example, by passing it to another function or returning it, or when the closure outlives the current scope and needs to own the data).
The compiler infers the mode, so you rarely need to annotate it. That inference is local and precise: the same closure can borrow a immutably and b mutably, as long as the borrows don’t conflict.
Immutable borrow
fn main() {
let list = vec![1, 2, 3];
let print_list = || println!("{:?}", list);
print_list();
println!("Still able to read list: {:?}", list); // fine
}
The closure only needs to read list, so it captures &list. The original list remains accessible.
Mutable borrow
fn main() {
let mut list = vec![1, 2, 3];
let mut add_item = || list.push(7);
add_item();
// println!("{:?}", list); // error: cannot borrow `list` as immutable
// because it is also borrowed as mutable
println!("After adding: {:?}", list); // okay after last use of add_item
}
Because the closure calls push on list, it needs &mut list. While that mutable borrow is active—i.e., while the closure exists and could be called—you cannot read or write list through any other path. Once you stop using the closure, the mutable borrow ends and you can access list again. The closure variable itself must be mut because calling a closure that captures a mutable reference is considered a mutation of the closure’s internal state.
Mutable captures block other access:
As long as a closure that mutably borrows a variable is still in scope, you cannot use that variable elsewhere. The borrow checker enforces this just like any other &mut borrow. If you need to interleave reads and writes, structure your code so the closure is dropped or not used during the conflicting sections.
Ownership (move)
A closure takes ownership when the body moves the value out of the captured variable (for example, returning it or sending it to another thread) or when you force ownership with the move keyword. Once ownership is transferred, the original variable cannot be used.
fn main() {
let message = String::from("hello");
let consume = || message; // moves message into closure
// println!("{}", message); // error: use of moved value
println!("{}", consume()); // prints "hello"
}
The closure body message (a return expression without a semicolon) moves the String out. That forces an ownership capture. After the closure is defined, message is no longer valid.
Accidental moves can break code:
If a closure unexpectedly moves a value—perhaps because you used it as a bare expression—the original variable becomes unusable. This often surprises beginners who think the closure will borrow. Check the closure body: any use that would move the value (returning it, passing it to a function that takes ownership, pattern matching that moves) triggers an ownership capture.
Multiple Captures in One Closure
A single closure can capture many variables, each with its own mode. The borrow checker enforces all of them simultaneously.
fn main() {
let threshold = 10;
let mut counter = 0;
let mut data = vec![5, 12, 3];
data.retain(|&item| {
if item > threshold { // immutable borrow of threshold
counter += 1; // mutable borrow of counter
true
} else {
false
}
});
println!("{} items above threshold", counter);
}
Here threshold is captured immutably and counter is captured mutably. The compiler checks that none of these borrows conflict and that the retain method (which accepts a FnMut closure) can call the closure multiple times while respecting the mutable borrow of counter. This all works because the closure does not hold any reference that outlives the call.
The move Keyword in Brief
Placing move before the parameter list forces the closure to take ownership of every variable it captures, regardless of how the body uses them.
let s = String::from("owned");
let own = move || println!("{}", s);
// println!("{}", s); // error: s has been moved
The most common use case is sending data to a new thread, where the closure must own the data to guarantee it outlives the current scope. That specific pattern is covered in depth in the next section, Closures That Borrow and That Steal (move Closures). For now, it’s enough to know that move overrides the compiler’s default capture inference and ensures ownership.
How Captures Relate to the Fn Traits
Every closure implements at least one of three traits: FnOnce, FnMut, and Fn. The one it implements depends directly on how it captures variables.
FnOnce– All closures implementFnOnce. A closure that moves captured values out of its body can only implementFnOnce, because it cannot be called a second time (the moved values are gone).FnMut– A closure that does not move captured values out implementsFnMut. It can mutate its captured references, so it can be called multiple times.Fn– A closure that neither moves values out nor mutates anything (i.e., only uses immutable borrows) implementsFn. It can be called multiple times, even concurrently.
These traits form a hierarchy: every Fn closure is also FnMut and FnOnce; every FnMut closure is also FnOnce. When you see a method like unwrap_or_else that requires FnOnce, you can pass any closure—even one that only borrows immutably—because Fn satisfies FnOnce.
Traits are automatically added:
You never write impl Fn for MyClosure. The compiler infers and implements the most permissive traits possible based on the closure’s capture behaviour. This automatic trait implementation is what makes closures seamlessly work with standard library methods that accept FnOnce, FnMut, or Fn.
The connection to capture is direct: the capture mode determines which trait bounds the closure satisfies. Later sections will explore function and closure types in more detail; for now, thinking of capture modes as the “why” behind the trait bounds will help you understand which closures can be passed where.
Common Mistakes
Using a variable after it has been moved into a closure
This happens when the closure captures by ownership (either explicitly with move or because the body moves the value). The compiler rejects any subsequent use of the moved variable.
let name = String::from("Alice");
let greet = || name; // moves name
println!("{}", name); // error: value moved
Not realising a closure captures the whole struct
When you capture a field like self.field, the closure borrows or moves the entire self. This can cause borrow conflicts if you try to access another field.
struct Point { x: i32, y: i32 }
let mut p = Point { x: 0, y: 0 };
let mut set_x = || p.x = 5; // mutably borrows all of p
println!("{}", p.y); // error: cannot borrow p as immutable
If you only need part of a struct, capture a reference to that part before the closure, or restructure the code so the closure only borrows what it needs.
Expecting a closure to borrow when it actually moves
A bare variable name as the final expression of a closure moves the value. If you intended to borrow, you must explicitly return a reference or use a different pattern.
let v = vec![1, 2, 3];
let get_v = || v; // moves v, closure now owns the Vec
// println!("{:?}", v); // error
If you only wanted to read v, write || &v (which captures v by reference and returns a reference) or simply || v.clone() if cloning is acceptable.
Practical Use Cases
Fallback values with unwrap_or_else: you already saw how a closure that captures self defers computation. This is a pattern that appears throughout the standard library—or_else on Option, unwrap_or_else on Result, and lazy initialisation crates.
Iterator adapters: closures are the backbone of iterator pipelines. A filter closure can capture a threshold value, a map closure can capture a multiplier, and a for_each closure can accumulate into an external counter.
let limit = 100;
let total = (1..=200).filter(|&n| n > limit).count();
// `limit` is captured by immutable reference inside the filter closure.
Callbacks that hold state: When a GUI framework or an async runtime asks you for a callback, you often need the callback to carry application state. Closures allow you to attach that state without a separate struct—the closure itself becomes the stateful callback.
let mut counter = 0;
let mut button = Button::new("Click me", move || {
counter += 1;
println!("Clicked {} times", counter);
});
button.press();
Here the closure captures counter mutably (and the move moves the closure into Button so it owns the counter).
Summary
Capturing the environment is the feature that separates closures from functions. The compiler’s automatic inference of borrow vs. move, and the resulting trait implementations (FnOnce, FnMut, Fn), let closures carry exactly as much context as they need without manual plumbing. Understanding the three capture modes—immutable borrow, mutable borrow, and ownership—gives you a mental model for predicting borrow-checker behaviour. The most frequent point of confusion is accidental moves, but once you recognise that a bare variable name in a closure body can transfer ownership, you can avoid it by returning references or cloning when necessary.