Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 24: Closures, Functions That Capture

You will understand

  • Closures as code + captured environment
  • Fn, FnMut, FnOnce — the callable trait family
  • Why move is needed at thread/async boundaries

Reading time

30 min
+ 20 min exercises
Capture Modes

A Closure Is Code Plus Environment

code|value| value > limitenvironmentlimit: 10captured by borrow or valuecapture behavior determines callable trait, not just syntax
Callable Family

Fn, FnMut, and FnOnce Reflect Capture Use

Fnreads captured datacall many timesFnMutmutates captureneeds &mut selfFnOnceconsumes captureone safe call

Readiness Check - Closure Capture and Trait Bounds

SkillLevel 0Level 1Level 2Level 3
Identify capture behaviorI treat closures as syntax sugar onlyI can tell when values are capturedI can explain borrow vs mutable borrow vs move captureI can design closure-heavy APIs with intentional capture strategy
Select callable boundsI guess between Fn/FnMut/FnOnceI know the rough differencesI can map call-site requirements to the right boundI can evolve abstractions without over-constraining callables
Use move at boundariesI add/remove move randomlyI know move captures by valueI can reason about thread/task boundary ownership correctlyI 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 codeWhat it usually meansTypical fix direction
E0373Closure may outlive current scope while borrowing local dataUse move and own captured values for 'static boundary requirements
E0525Closure trait mismatch (expected Fn/FnMut, got more restrictive closure)Reduce consumption/mutation, or relax API bound to required trait
E0382Captured value moved and then used laterClone 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:

  • Fn for shared access
  • FnMut for mutable access
  • FnOnce for 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

  1. threshold is a local i32.
  2. The closure uses it without moving or mutating it.
  3. The compiler captures threshold by shared borrow or copy-like semantics as appropriate.
  4. 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::spawn
  • tokio::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
  • tracing instrumentation 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 Fn bounds when FnMut or FnOnce are not needed
  • documenting why move is 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, and FnOnce describe what the closure needs from that environment.
  • move captures 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

QuestionAnswer
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

NeedBound or toolWhy
Reusable read-only callbackFnno mutation or consumption
Stateful callbackFnMutmutable captured state
One-shot consuming callbackFnOncecaptured ownership is consumed
Spawn thread/task with capturesmove closureown the environment
Hide closure concrete typeimpl Fn or Box<dyn Fn>opaque or dynamic callable