Closures and Rust's Fn Traits
A closure in Rust looks like a small, informal thing - a |x| x + 1 dropped inline into a map call - but under the hood it is a fully-typed struct that the compiler generates for you, with fields for whatever it captured from its surroundings and a call method for its body. That gap between how casually closures read and how precisely they are typed is the source of most of the confusion around them, and it is exactly where Fn, FnMut, and FnOnce come in.
This page is the conceptual anchor for the closures-and-functional-patterns section. Rather than walking through closure syntax step by step - that is what Closures Basics does - it explains what a closure actually is to the compiler, how environment capture decides which of the three call traits applies, and why treating Fn/FnMut/FnOnce as an ownership hierarchy, not three unrelated labels, is the fastest way to stop guessing at closure-related compile errors.
Summary
- A closure is an anonymous struct holding its captured environment, plus an implementation of one of three traits describing how it may be called.
- Insight: Passing behavior as a value - a callback, a comparator, a transformation - needs a way to bundle state with code, and Rust does it without a garbage collector by making that bundling a normal, ownership-checked struct.
- Key Concepts: capture, environment,
Fn/FnMut/FnOncetrait hierarchy,move, closure type inference. - When to Use: Any time behavior needs to travel with data - iterator adapters, callbacks, sorting comparators, deferred work, thread bodies.
- Limitations/Trade-offs: Each closure has a distinct, often unnameable type, which is why storing heterogeneous closures needs
Box<dyn Fn...>; capturing by move can force unwanted clones when a borrow would have been enough. - Related Topics: iterator adapters, generics and trait bounds, ownership and borrowing, thread spawning.
Foundations
When the compiler sees |x| x + factor, it does two things: it figures out what free variables the closure body refers to (here, factor), and it generates a unique, nameless struct type with a field for each one. Calling the closure is calling a method on that struct - conceptually, |x| x + factor compiles to something like:
struct Closure<'a> { factor: &'a i32 }
impl<'a> Closure<'a> {
fn call(&self, x: i32) -> i32 { x + *self.factor }
}This is why every closure has its own type, even two closures with identical-looking bodies. It is also why closures can be small and fast: there is no boxing, no vtable, and no heap allocation unless you explicitly ask for one (for example with Box<dyn Fn(...)>).
The capture step is where ownership enters the picture. By default, the compiler captures each variable in the least invasive way the closure body allows: by shared reference if the body only reads it, by mutable reference if the body mutates it, and by value only if the body needs to own it (for example, moving it into a returned value, or sending it to another thread). The move keyword overrides this inference and forces every captured variable to be taken by value, regardless of what the body would otherwise need.
Mechanics & Interactions
Once a closure's capture mode is decided, the compiler assigns it exactly one of three traits, chosen as the least restrictive interface the closure's body requires:
Fn : call as many times as you like, via &self (reads captures)
FnMut : call as many times as you like, via &mut self (reads/writes captures)
FnOnce : call at most once, via self (may consume captures)
These are not three disjoint categories - they form a hierarchy. Every Fn closure is also a valid FnMut (calling it through &mut self is strictly less demanding than calling it through &self), and every FnMut closure is also a valid FnOnce. So FnOnce is the trait every closure implements at minimum, and Fn is the strongest guarantee: it can be called repeatedly through a shared reference because it never mutates or consumes what it captured. This is why generic APIs that want maximum flexibility from their caller bound their parameter as loosely as possible - impl FnOnce() for something called exactly once, impl FnMut() for something called repeatedly with internal state, impl Fn() only when the closure genuinely never needs mutation.
The trait the compiler picks is driven entirely by what the closure's body does with its captures, not by anything you write explicitly. A closure that only reads a captured Vec is Fn. The same closure, if you add .push(...) to its body, becomes FnMut - the trait selection changed because the access pattern changed, even though nothing about the surrounding code did. This is also why thread::spawn requires FnOnce + Send + 'static: a spawned closure runs exactly once on another thread, its captures must be fully owned (no dangling borrows across the thread boundary), and move is almost always required to satisfy that.
// Same shape, different trait, because of what the body does:
let log = vec![String::from("start")];
let read_only = || println!("{log:?}"); // Fn: only reads `log`
let mut counter = 0;
let mutate = || { counter += 1; }; // FnMut: writes `counter`
let consume = move || drop(log); // FnOnce: takes ownership of `log`Each of these three closures has a different, incompatible type from the compiler's point of view, even though they all "look like closures." A function that wants to accept any of them generically writes fn run(f: impl FnOnce()), the loosest bound that covers all three, since every Fn and FnMut closure is also a valid FnOnce.
Advanced Considerations & Applications
Because each closure has a distinct, compiler-generated type, storing several different closures in one place - a list of callbacks, a plugin table - cannot use a plain Vec<SomeClosureType>. The two standard escapes are generics (when the set of closures is known at compile time and monomorphization is acceptable) and trait objects (Box<dyn Fn(...)> or Box<dyn FnMut(...)>, when the set is dynamic or must be stored uniformly). Trait objects add an indirection and a vtable call; generics avoid that cost but produce one specialized copy of the calling code per distinct closure type, trading binary size for speed.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
Generic impl Fn/FnMut/FnOnce | Zero-cost, inlines fully, no allocation | One monomorphized copy per closure type, can't store mixed closures in a Vec | Hot paths, iterator adapters, single-closure APIs |
Box<dyn Fn(...)> / Box<dyn FnMut(...)> | Uniform type, storable in collections, dynamic dispatch | Heap allocation, vtable call overhead, loses some inlining | Callback registries, plugin-style APIs, heterogeneous closure lists |
Plain fn pointer | Smallest, no capture at all | Cannot capture any environment | Stateless callbacks, C-compatible function pointers |
FnMut closures stored across calls are a common source of subtle bugs: a Box<dyn FnMut()> retained in a struct field means every call can mutate the closure's captured state, so two owners of the same boxed closure (which isn't possible without Rc<RefCell<...>> or similar) would need the same interior-mutability treatment as any other shared mutable state. This is one reason closures and smart pointers show up together so often - a Rc<RefCell<FnMut closure>> pattern is just the closure equivalent of any other shared-mutable-state problem.
Sorting APIs are a good illustration of the hierarchy's practical bite: slice::sort_by takes FnMut(&T, &T) -> Ordering because a comparator may be called many times and might reasonably carry counters or caches, but it never needs to be called through &mut self in a way that consumes anything, so FnOnce would be too weak a bound (it wouldn't allow repeated calls) and Fn would be an unnecessarily strong requirement on the caller.
Common Misconceptions
- "
Fn,FnMut, andFnOnceare three separate kinds of closure" - They are nested capability levels on the same closure, not a three-way split; the compiler picks the strongest one the closure's body actually satisfies, and everyFnis automatically alsoFnMutandFnOnce. - "
moveandFnOncemean the same thing" -movecontrols how variables are captured (by value, at closure-creation time); theFntraits describe how the closure may be called (by ref, mut ref, or by value). Amoveclosure that only reads its captures is stillFn, notFnOnce. - "A closure's type is
Fn,FnMut, orFnOnce" - Those are traits the closure's unique, compiler-generated type implements; the concrete type itself is anonymous and effectively unnameable outside the compiler. - "Closures always allocate on the heap" - A closure is a plain stack-allocated struct like any other, unless you explicitly box it (
Box<dyn Fn...>) to erase its concrete type for storage or dynamic dispatch. - "You choose whether a closure is
Fn,FnMut, orFnOnce" - You don't declare it; the compiler infers it strictly from what the closure body does with its captures. The only lever you control directly is capture mode viamove.
FAQs
What is a closure, structurally, to the Rust compiler?
An anonymous struct with one field per captured variable, plus a generated method implementing the call. Two closures with identical-looking bodies still have distinct types, because each is compiled independently.
How does Rust decide whether to capture a variable by reference or by value?
By inspecting what the closure body does with it: read-only access captures by shared reference, mutation captures by mutable reference, and only ownership-requiring use (or an explicit move) captures by value. The compiler picks the least invasive mode the body actually needs.
What does `move` actually change?
It forces every captured variable to be taken by value instead of by reference, regardless of what the closure body strictly requires. It does not, by itself, determine whether the closure is Fn, FnMut, or FnOnce - a move closure that only reads its owned captures can still be Fn.
Why are `Fn`, `FnMut`, and `FnOnce` described as a hierarchy instead of three separate traits?
Because Fn: FnMut and FnMut: FnOnce - every Fn closure is automatically a valid FnMut, and every FnMut is automatically a valid FnOnce. This lets generic code bound its parameter as loosely as it needs (FnOnce for "callable once," Fn for "callable repeatedly without mutation") and accept any closure that satisfies at least that level.
Why does `thread::spawn` require `FnOnce + Send + 'static`?
The spawned closure runs exactly once on another thread (FnOnce), its captured data must be safely transferable across the thread boundary (Send), and it cannot borrow anything that might not outlive the thread ('static). move is typically needed to satisfy the ownership half of this, since a plain borrow could dangle once the spawning function returns.
Can a closure's inferred trait change if I edit its body without touching its signature?
Yes - adding a mutation (like .push(...)) to a closure that previously only read its captures upgrades its required trait from Fn to FnMut, purely because of what the body now does. Nothing about how the closure is declared has to change for this to happen.
Why can't I put several different closures into one `Vec`?
Because each closure has its own distinct, compiler-generated type, even if their signatures match. A Vec<T> needs one concrete T, so mixed closures need to be unified either through generics (if the set is fixed and known) or through a trait object like Box<dyn Fn(...)>, which erases the concrete type behind a vtable.
What's the practical cost difference between a generic closure parameter and `Box`?
A generic impl Fn(...) parameter monomorphizes - the compiler generates a specialized copy of the calling code per distinct closure type, which usually inlines fully and costs nothing extra at runtime, at the price of larger binaries. Box<dyn Fn(...)> erases the type behind one shared vtable, costing a heap allocation and an indirect call but allowing storage of many different closures uniformly.
Why does `slice::sort_by` take `FnMut` instead of `Fn` or `FnOnce`?
A comparator may be invoked many times during a sort (ruling out FnOnce), but there's no reason to forbid it from carrying mutable internal state like a counter or cache (which Fn would forbid), so FnMut is exactly the right level of restriction - callable repeatedly, allowed to mutate its captures.
Is calling a `FnOnce` closure twice a runtime error or a compile error?
A compile error. The FnOnce::call_once method takes self by value, so after the first call the closure value has been consumed; the compiler statically rejects any code path that tries to call it again, the same way it rejects using any other moved-from value.
Do function pointers (`fn(...)`) relate to the `Fn` traits?
Yes - a plain fn pointer (which captures nothing) implements Fn, FnMut, and FnOnce simultaneously, since it satisfies all three call modes trivially. It's the special case of a "closure" with an empty environment, which is also why generic code bounded by Fn traits accepts ordinary functions, not just closures with |...| syntax.
Why does storing a `FnMut` closure in a struct sometimes need `RefCell` or similar?
Calling a FnMut closure requires a mutable reference to it (&mut self), so if the closure is reachable through a shared reference to its owner (for example behind an Rc), you need interior mutability to get that &mut access - the same requirement that applies to mutating any other field reachable only through &self.
Related
- Closures Basics - hands-on syntax and capture examples that build on this page's mental model.
- Fn, FnMut & FnOnce - a recipe-driven, side-by-side look at bounding closure parameters with each trait.
- move Closures - when and why
moveis required, especially for threads and async blocks. - Higher-Order Functions - passing and returning closures as ordinary values.
- Iterator Adapters - the most common place closures and the
Fntraits meet in everyday code.
Stack versions: This page is conceptual and not tied to a specific stack version.