Rust Closures and Fn Traits Explained
Master Rust closures, understand Fn, FnMut, and FnOnce traits, learn how closures capture variables, and use closures effectively in your code.
What you'll learn
- ✓How closures capture variables from their environment
- ✓The difference between Fn, FnMut, and FnOnce
- ✓How the compiler chooses which trait to implement
- ✓Practical patterns for closures in APIs and iterators
Prerequisites
- •Basic Rust knowledge
- •Understanding of ownership and borrowing
Closures are anonymous functions that capture variables from their surrounding scope. They are everywhere in Rust: iterators, thread spawning, callbacks, and custom APIs all rely on them. Understanding how closures work and the three Fn traits they implement is key to writing idiomatic Rust.
Closure Basics
A closure looks like a pair of pipes || for parameters, followed by the body:
fn main() {
// Simple closure
let add = |a: i32, b: i32| a + b;
println!("{}", add(3, 4)); // 7
// Closure with a block body
let greet = |name: &str| {
let message = format!("Hello, {name}!");
println!("{message}");
};
greet("Rust");
// Type inference: the compiler figures out types from usage
let double = |x| x * 2;
println!("{}", double(5)); // 10
}
Unlike regular functions, closures can capture variables from their enclosing scope:
fn main() {
let multiplier = 3;
let multiply = |x| x * multiplier; // captures multiplier
println!("{}", multiply(7)); // 21
}
How Closures Capture Variables
Closures capture variables in one of three ways, and the compiler picks the least restrictive option that works:
1. By Shared Reference (&T)
When the closure only reads the captured variable:
fn main() {
let name = String::from("Alice");
let greet = || println!("Hello, {name}"); // captures &name
greet();
greet();
// name is still usable because it was only borrowed
println!("Name is: {name}");
}
2. By Mutable Reference (&mut T)
When the closure modifies the captured variable:
fn main() {
let mut count = 0;
let mut increment = || {
count += 1; // captures &mut count
println!("Count: {count}");
};
increment(); // Count: 1
increment(); // Count: 2
// count is usable again after the closure's last use
println!("Final: {count}"); // Final: 2
}
3. By Value (move)
When the closure takes ownership of the captured variable:
fn main() {
let name = String::from("Alice");
let greet = move || println!("Hello, {name}"); // takes ownership of name
greet();
// This would fail: name was moved into the closure
// println!("{name}");
}
The move keyword forces the closure to take ownership of all captured variables. This is required when the closure outlives the scope where the variable was defined, such as when spawning threads:
use std::thread;
fn main() {
let data = vec![1, 2, 3];
// Without move, this would fail: data might be dropped
// before the thread finishes
let handle = thread::spawn(move || {
println!("Data: {:?}", data);
});
handle.join().unwrap();
}
The Three Fn Traits
The compiler automatically implements one or more of three traits for each closure, based on how it uses captured variables:
FnOnce
Every closure implements FnOnce. It means the closure can be called at least once. Closures that consume a captured value (move it out) can only be called once:
fn call_once<F: FnOnce() -> String>(f: F) -> String {
f() // consumes f
}
fn main() {
let name = String::from("Alice");
// This closure moves name out, so it can only be called once
let make_greeting = || format!("Hello, {name}");
let greeting = call_once(make_greeting);
println!("{greeting}");
// Cannot call make_greeting again: it was consumed by call_once
}
FnMut
Closures that mutate captured variables (but do not consume them) implement FnMut. They can be called multiple times:
fn call_twice<F: FnMut()>(mut f: F) {
f();
f();
}
fn main() {
let mut total = 0;
call_twice(|| {
total += 10;
});
println!("Total: {total}"); // 20
}
Fn
Closures that only read captured variables (or capture nothing) implement Fn. They can be called any number of times and shared between threads:
fn apply_to_all<F: Fn(i32) -> i32>(values: &[i32], f: F) -> Vec<i32> {
values.iter().map(|&x| f(x)).collect()
}
fn main() {
let factor = 3;
let results = apply_to_all(&[1, 2, 3, 4], |x| x * factor);
println!("{:?}", results); // [3, 6, 9, 12]
}
The Trait Hierarchy
The traits form a hierarchy: Fn is a subtrait of FnMut, which is a subtrait of FnOnce.
// Fn: &self -> can call any number of times, no mutation
// FnMut: &mut self -> can call any number of times, may mutate
// FnOnce: self -> can call at least once, may consume
// If you need the most flexible parameter, use FnOnce
// If you need to call multiple times, use FnMut
// If you need to call multiple times without mutation, use Fn
This means a closure that implements Fn also implements FnMut and FnOnce. You can pass an Fn closure anywhere an FnOnce is expected.
Closures in Practice
With Iterators
Iterators are the most common place you will use closures:
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let result: Vec<i32> = numbers
.iter()
.filter(|&&x| x % 2 == 0) // Fn: only reads x
.map(|&x| x * x) // Fn: only reads x
.collect();
println!("{:?}", result); // [4, 16, 36, 64, 100]
// fold uses FnMut because it mutates the accumulator
let sum = numbers.iter().fold(0, |acc, &x| acc + x);
println!("Sum: {sum}"); // 55
}
Returning Closures
To return a closure from a function, use impl Fn or Box<dyn Fn>:
// impl Fn: static dispatch, single concrete type
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
// Box<dyn Fn>: dynamic dispatch, can return different closures
fn make_operation(op: &str) -> Box<dyn Fn(i32, i32) -> i32> {
match op {
"add" => Box::new(|a, b| a + b),
"mul" => Box::new(|a, b| a * b),
_ => Box::new(|a, _b| a),
}
}
fn main() {
let add_five = make_adder(5);
println!("{}", add_five(10)); // 15
let op = make_operation("mul");
println!("{}", op(6, 7)); // 42
}
Storing Closures in Structs
Use generics or trait objects to store closures in structs:
// Generic: monomorphized, faster
struct Callback<F: Fn(i32)> {
func: F,
}
impl<F: Fn(i32)> Callback<F> {
fn call(&self, value: i32) {
(self.func)(value);
}
}
// Trait object: flexible, can store different closures
struct EventHandler {
handlers: Vec<Box<dyn Fn(&str)>>,
}
impl EventHandler {
fn new() -> Self {
EventHandler { handlers: vec![] }
}
fn on_event(&mut self, handler: impl Fn(&str) + 'static) {
self.handlers.push(Box::new(handler));
}
fn emit(&self, event: &str) {
for handler in &self.handlers {
handler(event);
}
}
}
fn main() {
let mut events = EventHandler::new();
events.on_event(|e| println!("Logger: {e}"));
events.on_event(|e| println!("Monitor: {e}"));
events.emit("user_login");
// Logger: user_login
// Monitor: user_login
}
Closures as Strategy Pattern
struct Sorter<F: Fn(&str, &str) -> std::cmp::Ordering> {
compare: F,
}
impl<F: Fn(&str, &str) -> std::cmp::Ordering> Sorter<F> {
fn sort(&self, items: &mut [String]) {
items.sort_by(|a, b| (self.compare)(a, b));
}
}
fn main() {
let mut names = vec![
"Charlie".to_string(),
"alice".to_string(),
"Bob".to_string(),
];
// Case-insensitive sort
let sorter = Sorter {
compare: |a: &str, b: &str| a.to_lowercase().cmp(&b.to_lowercase()),
};
sorter.sort(&mut names);
println!("{:?}", names); // ["alice", "Bob", "Charlie"]
}
Choosing the Right Fn Trait
When writing functions or structs that accept closures:
- Use
Fnif you need to call the closure multiple times and it should not mutate anything. This is the most restrictive but most flexible for callers. - Use
FnMutif the closure needs to mutate its captured state. Iterator methods likefolduse this. - Use
FnOnceif you only call the closure once. This is the least restrictive and accepts all closures. Use it when you can.
A good default: accept FnOnce when you only call the closure once, and Fn when you call it multiple times.
Wrapping Up
Closures in Rust capture variables by shared reference, mutable reference, or by value. The compiler picks the least restrictive mode automatically, and the move keyword forces ownership transfer. The three Fn traits (Fn, FnMut, FnOnce) describe how the closure uses its captures. Understanding this system lets you write flexible APIs, use iterators effectively, and build callback-driven architectures that are both safe and performant.
Related articles
- Rust Rust Iterators and Closures: Functional Programming
Master Rust iterators and closures for functional-style programming. Covers map, filter, fold, chaining, custom iterators, and Fn trait family.
- Rust Rust Async Programming with Tokio: Getting Started
Learn async Rust with Tokio from scratch. Covers async/await syntax, spawning tasks, channels, TCP servers, and common pitfalls with working examples.
- Rust Rust Concurrency: Channels, Mutex, and Arc Patterns
Learn Rust concurrency with channels, Mutex, Arc, and thread-safe patterns. Covers mpsc, shared state, thread pools, and real-world examples.
- Rust Rust Error Handling with anyhow and thiserror
Master Rust error handling with anyhow for applications and thiserror for libraries. Practical patterns, conversions, and context-rich error reporting.