Chapter 24: Closures, Functions That Capture
Prerequisites
You will understand
- Closures as code + captured environment
Fn,FnMut,FnOnce— the callable trait family- Why
moveis needed at thread/async boundaries
Reading time
A Closure Is Code Plus Environment
Fn, FnMut, and FnOnce Reflect Capture Use
Readiness Check - Closure Capture and Trait Bounds
| Skill | Level 0 | Level 1 | Level 2 | Level 3 |
|---|---|---|---|---|
| Identify capture behavior | I treat closures as syntax sugar only | I can tell when values are captured | I can explain borrow vs mutable borrow vs move capture | I can design closure-heavy APIs with intentional capture strategy |
| Select callable bounds | I guess between Fn/FnMut/FnOnce | I know the rough differences | I can map call-site requirements to the right bound | I can evolve abstractions without over-constraining callables |
Use move at boundaries | I add/remove move randomly | I know move captures by value | I can reason about thread/task boundary ownership correctly | I can avoid both unnecessary clones and invalid borrows in concurrent code |
Target Level 2+ before advanced async callback orchestration.
Compiler Error Decoder - Closures and Capture
| Error code | What it usually means | Typical fix direction |
|---|---|---|
| E0373 | Closure may outlive current scope while borrowing local data | Use move and own captured values for 'static boundary requirements |
| E0525 | Closure trait mismatch (expected Fn/FnMut, got more restrictive closure) | Reduce consumption/mutation, or relax API bound to required trait |
| E0382 | Captured value moved and then used later | Clone before move boundary or redesign ownership so post-use is unnecessary |
When closures fail to type-check, inspect capture mode first, then callable trait bound, then lifetime boundary.
Step 1 - The Problem
Many APIs need behavior as input:
- iterator predicates
- sorting keys
- retry policies
- callbacks
- task bodies
Ordinary functions can express some of this, but they cannot naturally carry local context. Closures solve that problem by capturing values from the surrounding environment.
Step 2 - Rust’s Design Decision
Rust closures are not one opaque callable kind. They are classified by how they capture:
Fnfor shared accessFnMutfor mutable accessFnOncefor consuming captured values
Rust accepted:
- more trait names to learn
- a more explicit capture model
Rust refused:
- hiding movement or mutation cost behind a generic “callable” abstraction
Step 3 - The Mental Model
Plain English rule: a closure is code plus an environment, and the way it uses that environment determines which callable traits it implements.
Step 4 - Minimal Code Example
#![allow(unused)]
fn main() {
let threshold = 10;
let is_large = |value: &i32| *value > threshold;
assert!(is_large(&12));
}
Step 5 - Line-by-Line Compiler Walkthrough
thresholdis a locali32.- The closure uses it without moving or mutating it.
- The compiler captures
thresholdby shared borrow or copy-like semantics as appropriate. - The closure can be called repeatedly, so it implements
Fn.
Now compare:
#![allow(unused)]
fn main() {
let mut seen = 0;
let mut record = |_: i32| {
seen += 1;
};
}
This closure mutates captured state, so it requires FnMut.
And:
#![allow(unused)]
fn main() {
let name = String::from("worker");
let consume = move || name;
}
This closure moves name out when called, so it is only FnOnce.
Step 6 - Three-Level Explanation
Closures can use values from the place where they were created. That is what makes them useful for filters, callbacks, and tasks.
Most iterator closures are Fn or FnMut. Thread and async task closures often need move because the closure must own the captured values across the new execution boundary.
This is why move shows up so often in:
thread::spawntokio::spawn- callback registration
A closure is a compiler-generated struct plus one or more trait impls from the Fn* family. Captured variables become fields. The call operator lowers to methods on those traits. This is why closure capture mode is part of the type story, not just syntax sugar.
move Closures
move does not mean “copy everything.” It means “capture by value.”
For Copy types, that looks like a copy.
For owned non-Copy values, it means a move.
That distinction matters because move is often the right choice at execution-boundary APIs, but it can also change the closure from Fn or FnMut to FnOnce depending on how the captured fields are used.
Closures as Parameters and Returns
You will see:
#![allow(unused)]
fn main() {
fn apply<F>(value: i32, f: F) -> i32
where
F: Fn(i32) -> i32,
{
f(value)
}
}
And sometimes:
#![allow(unused)]
fn main() {
fn make_checker(limit: i32) -> impl Fn(i32) -> bool {
move |x| x > limit
}
}
Returning closures by trait object is possible too:
#![allow(unused)]
fn main() {
fn make_boxed() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
}
Use trait objects when runtime erasure is useful. Use impl Fn when one concrete closure type is enough.
Step 7 - Common Misconceptions
Wrong model 1: “Closures are just anonymous functions.”
Correction: they are anonymous function-like values with captured environment.
Wrong model 2: “move copies the environment.”
Correction: it captures by value, which may mean move or copy depending on the type.
Wrong model 3: “FnOnce means the closure always gets called exactly once.”
Correction: it means the closure may consume captured state and therefore can only be called once safely.
Wrong model 4: “If a closure compiles in an iterator, it will work in a thread spawn.”
Correction: thread boundaries impose stronger ownership and often Send + 'static constraints.
Step 8 - Real-World Pattern
Closures are everywhere in idiomatic Rust:
- iterator adapters
- sort comparators
- retry wrappers
tracinginstrumentation helpers- async task bodies
Strong Rust code relies on closures heavily, but it also respects their ownership behavior instead of treating them as syntactic sugar over lambdas from other languages.
Step 9 - Practice Block
Code Exercise
Write one closure that implements Fn, one that implements FnMut, and one that is only FnOnce. Explain why each falls into that category.
Code Reading Drill
What does this closure capture, and how?
#![allow(unused)]
fn main() {
let prefix = String::from("id:");
let format_id = |id: u32| format!("{prefix}{id}");
}
Spot the Bug
Why does this fail after the spawn?
#![allow(unused)]
fn main() {
let data = String::from("hello");
let handle = std::thread::spawn(move || data);
println!("{data}");
}
Refactoring Drill
Take a named helper function that only exists to capture one local configuration value and rewrite it as a closure if that improves locality.
Compiler Error Interpretation
If the compiler says a closure only implements FnOnce, translate that as: “this closure consumes part of its captured environment.”
Step 10 - Contribution Connection
After this chapter, you can read:
- iterator-heavy closures
- task and thread bodies
- higher-order helper APIs
- boxed callback registries
Good first PRs include:
- removing unnecessary clones around closures
- choosing narrower
Fnbounds whenFnMutorFnOnceare not needed - documenting why
moveis required at a boundary
In Plain English
Closures are little bundles of behavior and remembered context. Rust cares about exactly how they remember that context because borrowing, mutation, and ownership still matter even when code is passed around like data.
What Invariant Is Rust Protecting Here?
Closure calls must respect how captured data is borrowed, mutated, or consumed, so callable reuse stays consistent with ownership rules.
If You Remember Only 3 Things
- A closure is code plus captured environment.
Fn,FnMut, andFnOncedescribe what the closure needs from that environment.movecaptures by value; it does not guarantee copying.
Memory Hook
A closure is a backpacked function. What is in the backpack, and whether it gets borrowed, edited, or emptied, determines how often the traveler can keep walking.
Flashcard Deck
| Question | Answer |
|---|---|
| What extra thing does a closure have that a plain function usually does not? | Captured environment. |
What does Fn mean? | The closure can be called repeatedly without mutating or consuming captures. |
What does FnMut mean? | The closure may mutate captured state between calls. |
What does FnOnce mean? | The closure may consume captured state and therefore can only be called once safely. |
What does move do? | Captures values by value rather than by borrow. |
Why is move common in thread or task APIs? | The closure must own its captured data across the execution boundary. |
Can a closure implement more than one Fn* trait? | Yes. A non-consuming closure can implement Fn, FnMut, and FnOnce hierarchically. |
When might you return Box<dyn Fn(...)>? | When you need runtime-erased callable values with a uniform interface. |
Chapter Cheat Sheet
| Need | Bound or tool | Why |
|---|---|---|
| Reusable read-only callback | Fn | no mutation or consumption |
| Stateful callback | FnMut | mutable captured state |
| One-shot consuming callback | FnOnce | captured ownership is consumed |
| Spawn thread/task with captures | move closure | own the environment |
| Hide closure concrete type | impl Fn or Box<dyn Fn> | opaque or dynamic callable |