The shape of the problem
You've written a function that filters numbers. Now you need one that filters strings. Then a third that filters dates. The logic is identical; only the condition changes. In JavaScript, you'd pass a callback to filter without thinking. Python lets you shove a lambda anywhere. Rust lets you do the same, but the type system stops you and asks for a signature. The syntax looks like a puzzle at first. Once you see the three closure traits, the puzzle clicks into place.
Closures are structs with permissions
A closure in Rust is an anonymous struct the compiler generates for you. The struct holds whatever variables the closure captures from the surrounding scope. The struct implements one of three traits based on what the closure does with those variables. Think of the traits as permission levels for the captured data.
Fn is read-only access. You can call the closure as many times as you want. It never modifies the captured variables. FnMut borrows the captures mutably. You can call it many times, but each call might change the captured state. FnOnce consumes the captures. You call it once, and the captured values are gone.
Pick the loosest trait your function actually needs. Fn accepts anything. FnOnce accepts only closures that promise to be called at most once. Trust the borrow checker here. It forces you to be honest about what the closure does.
The minimal case
Here's the smallest case: a generic function that accepts any callable and applies it to a value.
/// Applies a transformation to a value.
fn apply<F>(x: i32, f: F) -> i32
where
// F must be callable with an i32 and return an i32.
// We only need to call it, so Fn is sufficient.
F: Fn(i32) -> i32,
{
// Calling a closure looks like calling any function.
f(x)
}
fn main() {
// Closure captures nothing. Implements Fn, FnMut, and FnOnce.
let add_one = |x| x + 1;
// Pass by value. The function uses it and drops it.
let result = apply(5, add_one);
println!("{result}");
}
Two things matter here. First, F is a generic type parameter. Each call site picks a concrete F at compile time. The compiler generates a specific version of apply for that closure type. The call f(x) becomes a direct jump to the closure body. No virtual dispatch. No heap allocation. The cost is zero. This is why Rust iterators are fast. The compiler inlines everything.
Second, Fn(i32) -> i32 is a trait bound. The parentheses syntax is the trait Fn parameterized by an argument tuple and a return type. It's still a regular trait under the hood. The compiler gives it nicer notation because closures are common.
Convention: impl Trait
You'll see two ways to write the signature. The generic form with a where clause is explicit. The impl Fn form is shorthand.
/// Shorthand for a generic closure parameter.
/// Compiles to the exact same code as the generic version.
fn apply_impl(x: i32, f: impl Fn(i32) -> i32) -> i32 {
f(x)
}
Use impl Fn(...) when you want shorter signatures and you're only accepting one closure parameter. Use the named generic <F: Fn(...)> form when you need to refer to F somewhere else, like in a return type or a where clause with multiple bounds. The community prefers impl Fn in argument position for readability. It desugars to the generic form. The compiler output is identical.
Mutating captures
Switch to FnMut when the closure updates state it captured.
/// Calls a closure for each item, allowing mutation of captures.
fn for_each_mut<F: FnMut(i32)>(items: &[i32], mut f: F) {
for &item in items {
// FnMut requires a mutable borrow to call.
f(item);
}
}
fn main() {
let mut sum = 0;
// Captures sum mutably. Implements FnMut, not Fn.
let add_to_sum = |x| sum += x;
for_each_mut(&[1, 2, 3], add_to_sum);
println!("sum = {sum}");
}
Notice the parameter is mut f: F. You need a mutable binding to call an FnMut. If you forget the mut, the compiler rejects you with E0596 (cannot borrow as mutable). The closure itself isn't changing. The data inside the closure is. The call site needs permission to mutate the closure struct.
Remember the mut on the parameter. The compiler won't guess you want to mutate the closure struct.
Consuming captures
Use FnOnce when the closure moves a non-Copy value out of its captures.
/// Accepts a closure that consumes its captures.
fn consume_with<F: FnOnce() -> String>(f: F) -> String {
f()
}
fn main() {
let name = String::from("Ferris");
// move forces capture by value. String is moved out on call.
let greet = move || format!("hello, {name}");
let msg = consume_with(greet);
println!("{msg}");
}
The move keyword forces the closure to take ownership of name. When the closure runs, it consumes name to build the string. The closure can only run once. If you try to call it again, you get E0382 (use of moved value). The compiler tracks the closure's ownership just like any other value.
If the closure moves a value, it dies after one call. Treat it like a single-use token.
A realistic retry loop
Here's a retry helper that shows FnMut in action.
/// Retries an action up to n times.
fn retry<F, T, E>(mut attempts: u32, mut action: F) -> Result<T, E>
where
// FnMut allows the action to update state across calls.
F: FnMut() -> Result<T, E>,
{
loop {
match action() {
Ok(value) => return Ok(value),
// Decrement attempts on failure.
Err(e) if attempts > 1 => attempts -= 1,
Err(e) => return Err(e),
}
}
}
fn main() {
let mut tries = 0;
// Captures tries mutably to track attempts.
let result: Result<&str, &str> = retry(3, || {
tries += 1;
if tries < 3 { Err("not yet") } else { Ok("done") }
});
println!("{result:?} after {tries} tries");
}
This pattern appears in network requests and file I/O. The closure captures tries and updates it. The function needs FnMut because the closure mutates state across calls. The generic bound makes the helper reusable for any action that returns a Result.
Generics make the helper reusable. The trait bound makes it safe.
Pitfalls and error codes
The most common error is capturing a move when the function expects FnMut. If your closure does let s = captured_string;, it moves the string. The closure becomes FnOnce. Passing it to a function expecting FnMut triggers E0507 (cannot move out of borrowed content). The fix is to borrow the string instead, or relax the function to accept FnOnce.
Another trap is passing a mutating closure to a function that asks for Fn. The compiler rejects this with E0525 (expected closure implementing Fn, found FnMut). Relax the bound to FnMut.
Read the error code. It tells you exactly which trait is missing.
Decision matrix
Use Fn when the closure only reads captured variables and you want maximum flexibility. Use FnMut when the closure updates state across multiple calls, like accumulating a sum or logging attempts. Use FnOnce when the closure consumes a captured value, such as moving a String or calling a function that takes ownership. Reach for Box<dyn Fn(...)> only when you must store closures of different types in a collection or return them from different branches. The generic form is faster and avoids heap allocation.
Default to generics. Dynamic dispatch is a tax you pay only when you have to.