Rust Closures Confused Me Until I Understood Fn, FnMut, and FnOnce
For a long time I wrote Rust closures the same way I wrote lambdas in Java or functions in Python: I defined them, passed them around, and…
Rust Closures Confused Me Until I Understood Fn, FnMut, and FnOnce

For a long time I wrote Rust closures the same way I wrote lambdas in Java or functions in Python: I defined them, passed them around, and mostly ignored the type system until the compiler started yelling at me. Then I would add move or change Fn to FnMut based on whatever the error message suggested, and the code would compile, and I would move on without fully understanding what had just happened.
That approach works until it does not. The moment you try to store a closure in a struct, pass one across a thread boundary, or return one from a function, the compiler demands you know exactly which of the three closure traits you are working with. And if you do not know the difference between Fn, FnMut, and FnOnce, the error messages start to feel personal.
The three traits are not arbitrary. They map directly to what a closure does with the variables it captures from the surrounding scope, and once that clicks, every compiler error about closures starts making immediate sense.
What a closure actually is
Before the traits make sense, the capture mechanism needs to be clear. A closure in Rust is a function that can capture variables from the environment where it is defined. When you write this:
fn main() {
let message = String::from("hello");
let print_it = || println!("{}", message);
print_it();
}
The closure print_it captures message from the surrounding scope. The question Rust asks is: how does it capture it? Does it borrow message immutably, borrow it mutably, or take ownership of it entirely? The answer determines which of the three Fn traits the closure implements, and that in turn determines where and how the closure can be used.
Rust infers the capture mode from what the closure actually does with the variable. If the closure only reads the variable, Rust captures by immutable reference. If it modifies the variable, Rust captures by mutable reference. If it consumes the variable (moves it or drops it), Rust captures by value. The move keyword forces ownership transfer regardless of what the closure does, which matters when you need the closure to outlive the scope where the variable was defined, like when sending a closure to a thread.
Fn: borrows immutably, can be called repeatedly
Fn is the most permissive trait. A closure implements Fn if it only captures variables by immutable reference, meaning it reads from the environment but never modifies or consumes anything it captured.
fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(f(x))
}
fn main() {
let multiplier = 3;
// This closure only reads 'multiplier', so it implements Fn
let triple = |x| x * multiplier;
println!("{}", apply_twice(triple, 2)); // 18: triple(triple(2)) = triple(6) = 18
// Because it's Fn, we can call it as many times as we want
println!("{}", triple(5)); // 15
println!("{}", triple(10)); // 30
}
The apply_twice function takes any closure implementing Fn(i32) -> i32 and calls it twice on the same input. This works because Fn closures can be called repeatedly without any side effects on the captured environment. The closure borrows multiplier for each call and releases it. Nothing changes between calls.
In Java terms, this is closest to a pure lambda that captures a final variable. In Python, it maps to a function that reads from the enclosing scope without modifying it.
The Fn bound is the right default when you do not know whether you need to call the closure once or many times, and when the closure does not need to modify its captured state. Most iterator adapters use Fn for exactly this reason: map, filter, and for_each all take Fn or FnMut because they need to call the closure once per element.
FnMut: borrows mutably, can be called repeatedly but changes state
FnMut is for closures that modify something they captured. The closure can be called multiple times, but each call may change the captured state.
fn apply_n_times<F: FnMut()>(mut f: F, n: usize) {
for _ in 0..n {
f();
}
}
fn main() {
let mut count = 0;
// This closure modifies 'count', so it implements FnMut
let increment = || {
count += 1;
println!("count is now {}", count);
};
apply_n_times(increment, 3);
// Output:
// count is now 1
// count is now 2
// count is now 3
}
Notice that apply_n_times takes mut f: F. The mut is required because calling a FnMut closure mutates it (it carries the mutable borrow of count inside it). The function parameter needs to be declared mutable to allow that.
The practical consequence of FnMut is that you cannot call it concurrently. If two threads both tried to call the same FnMut closure at the same time, they would both be trying to mutably borrow count simultaneously, which Rust will not allow. FnMut closures are single-threaded by nature unless you wrap the captured state in something like Arc<Mutex<T>>, at which point you are back to Fn because the closure itself is only reading the Arc pointer.
This trips people up when they try to share a FnMut closure across threads. The compiler refuses and the error message talks about Send bounds and Sync, which feels like a different problem entirely, but the root cause is the mutable capture. The fix is to redesign the captured state, not to fight the borrow checker.
FnOnce: takes ownership, can only be called once
FnOnce is for closures that consume something they captured. Once you call the closure, the captured value has been moved out of it and is gone. Calling it a second time would require reading a value that no longer exists, so the compiler prevents it at compile time.
fn consume_string<F: FnOnce() -> String>(f: F) -> String {
f() // Can only call this once
}
fn main() {
let greeting = String::from("hello from the closure");
// This closure moves 'greeting' out of itself when called
let get_greeting = || greeting; // moves greeting on call
let result = consume_string(get_greeting);
println!("{}", result);
// This would not compile:
// let result2 = consume_string(get_greeting);
// error: use of moved value: `get_greeting`
}
The closure get_greeting captures greeting and returns it by value. Returning a captured value moves it out of the closure. After the first call, greeting is gone. There is nothing left to return on a second call, so the type system prevents it.
FnOnce is the most restrictive trait in terms of how the closure can be used, but it is the most permissive in terms of what the closure can do. A FnOnce closure can capture by value, modify captured values, and consume them. Every closure implements FnOnce, because every closure can be called at least once. FnMut closures also implement FnOnce. Only Fn closures implement all three.
The hierarchy is: Fn is a subtype of FnMut, which is a subtype of FnOnce. A closure that implements Fn can be used anywhere a FnMut or FnOnce is expected. A closure that only implements FnOnce cannot be used where Fn or FnMut is expected.
// This accepts any closure that can be called at least once
fn call_once<F: FnOnce()>(f: F) {
f();
}
// This accepts closures that can be called multiple times without side effects
fn call_many_times<F: Fn()>(f: F) {
for _ in 0..5 {
f();
}
}
fn main() {
let name = String::from("world");
// FnOnce closure: consumes 'name' on call
let consume = move || println!("consuming: {}", name);
call_once(consume); // fine
// call_many_times(consume); // error: consume is FnOnce, not Fn
// Fn closure: only reads from environment
let prefix = "hello";
let greet = || println!("{}", prefix);
call_once(greet); // fine: Fn satisfies FnOnce
call_many_times(greet); // also fine: Fn satisfies Fn
}
The move keyword and when you actually need it
move forces the closure to take ownership of all captured variables, regardless of whether it would need to. This is the mechanism that makes closures safe to send to threads.
use std::thread;
fn main() {
let data = vec![1, 2, 3];
// Without move, this fails: data might be dropped before the thread finishes
let handle = thread::spawn(move || {
println!("data in thread: {:?}", data);
});
handle.join().unwrap();
// data is moved into the thread, so this would not compile:
// println!("{:?}", data);
}
Without move, the closure would capture data by reference. The thread could outlive the function that owns data, at which point the reference would be dangling. Rust catches this at compile time. With move, the closure owns data outright and the thread can safely use it for as long as it runs.
The move keyword does not change which Fn trait the closure implements. A move closure that only reads from its captured values still implements Fn. A move closure that modifies captured values still implements FnMut. What move changes is the lifetime of the captured data, not the access pattern.
Storing closures in structs
This is where the trait distinctions become unavoidable. When you want to store a closure in a struct, you need to declare which trait bound the closure satisfies, and you have two options: generic parameters or trait objects.
// Generic parameter: the struct is specialized for one concrete closure type
// Resolved at compile time, no runtime overhead
struct Transformer<F: Fn(i32) -> i32> {
func: F,
name: String,
}
impl<F: Fn(i32) -> i32> Transformer<F> {
fn new(name: &str, func: F) -> Self {
Transformer {
func,
name: name.to_string(),
}
}
fn apply(&self, value: i32) -> i32 {
(self.func)(value)
}
}
fn main() {
let doubler = Transformer::new("double", |x| x * 2);
let tripler = Transformer::new("triple", |x| x * 3);
println!("{}", doubler.apply(5)); // 10
println!("{}", tripler.apply(5)); // 15
// doubler and tripler are different types because their closures are different types
// You cannot put them in the same Vec<Transformer<??>>
}
The limitation of the generic approach is that each closure is a distinct type, so Transformer<|x| x * 2> and Transformer<|x| x * 3> are different types even though both satisfy Fn(i32) -> i32. You cannot put them in the same collection.
When you need a collection of closures with the same signature but different implementations, you use trait objects:
// Trait object: stores a pointer to the closure on the heap
// Resolved at runtime, small vtable overhead
struct Pipeline {
steps: Vec<Box<dyn Fn(i32) -> i32>>,
}
impl Pipeline {
fn new() -> Self {
Pipeline { steps: Vec::new() }
}
fn add_step<F: Fn(i32) -> i32 + 'static>(&mut self, step: F) {
self.steps.push(Box::new(step));
}
fn run(&self, input: i32) -> i32 {
self.steps.iter().fold(input, |acc, step| step(acc))
}
}
fn main() {
let mut pipeline = Pipeline::new();
pipeline.add_step(|x| x * 2);
pipeline.add_step(|x| x + 10);
pipeline.add_step(|x| x / 3);
println!("{}", pipeline.run(5)); // ((5 * 2) + 10) / 3 = 6
}
The 'static bound on add_step means the closure cannot contain references that might be dropped before the Pipeline is. For closures that own their captured data (via move), this is automatically satisfied. For closures that capture references, you would need to ensure the lifetimes work out, which usually means redesigning the capture.
The choice between generic parameters and trait objects is a genuine trade-off. Generic parameters give you zero runtime overhead because the compiler generates specialised code for each concrete closure type. Trait objects add a heap allocation and a vtable lookup per call, which is measurable in tight loops. For a pipeline that runs occasionally, the difference is irrelevant. For a hot path processing millions of items per second, it matters.
Returning closures from functions
Returning a closure from a function requires either impl Fn syntax or a Box<dyn Fn>. You cannot return a bare F: Fn because the caller would need to know the concrete type of the closure, and closure types are anonymous in Rust.
// impl Fn: zero-cost, but the return type is opaque
// Every call to make_adder returns the same concrete type
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
// Box<dyn Fn>: heap allocation, but allows returning different closure types
fn make_operation(add: bool) -> Box<dyn Fn(i32) -> i32> {
if add {
Box::new(|x| x + 1)
} else {
Box::new(|x| x * 2)
}
}
fn main() {
let add_five = make_adder(5);
println!("{}", add_five(10)); // 15
println!("{}", add_five(20)); // 25
let op = make_operation(true);
println!("{}", op(10)); // 11
let op = make_operation(false);
println!("{}", op(10)); // 20
}
impl Fn works when every code path returns the same closure type. The compiler knows the concrete type at compile time and can inline and optimise it. Box<dyn Fn> works when different branches return different closure types, because the Box erases the concrete type and the vtable handles dispatch at runtime. The make_operation function above cannot use impl Fn because the two branches return closures with different types, and impl Fn requires a single concrete return type.
Where this shows up in real Rust code
Understanding the three traits clarifies a class of compiler errors that previously seemed arbitrary.
When you see the trait bound FnMut is not satisfied, you have passed a closure that modifies captured state to a function that expected a side-effect-free Fn. The fix is either to change the bound to FnMut or to redesign the closure so it does not need mutable access.
When you see cannot move out of captured variable in an Fn closure, you have tried to consume a captured value inside a closure that is supposed to implement Fn or FnMut. The closure cannot be called more than once if it consumes the value. Either clone the value before consuming it, or change the bound to FnOnce if the closure genuinely only needs to run once.
When you see closure may outlive the current function, you have captured a reference in a closure that might live longer than the reference's owner, typically when spawning threads or storing the closure. The fix is move, which takes ownership instead of borrowing.
These are not arbitrary restrictions. They are the borrow checker enforcing the same ownership rules that apply everywhere else in Rust, applied to captured variables. The closure traits are the vocabulary for expressing those rules at the type level, and once you read them that way, the errors stop feeling like obstacles and start reading as accurate descriptions of the problem.
If closures are clicking and you want to go deeper into iterators, the stack and heap, and how Rust handles concurrency differently from every other language you have used, the field guide covers all of it.
$7 — Zero to Rust: A Systems Programmer’s Field Guide You can read the free sample here — Free 20-page sample
메타데이터
- post_id
- 4bcd6a8f56d0
- slug
- rust-closures-confused-me-until-i-understood-fn-fnmut-and-fnonce-4bcd6a8f56d0
- url
- https://medium.com/zero-to-rust-go-from-beginner-to-rust-expert/rust-closures-confused-me-until-i-understood-fn-fnmut-and-fnonce-4bcd6a8f56d0
- canonical_url
- https://medium.com/zero-to-rust-go-from-beginner-to-rust-expert/rust-closures-confused-me-until-i-understood-fn-fnmut-and-fnonce-4bcd6a8f56d0
- author_url
- https://medium.com/@zeeshankhan0094
- status
- ok
- fetched_at
- 2026-07-09 17:12:49