Closures are anonymous functions that can capture variables from their surrounding scope. They are first-class values in Rust — you can store them in variables, pass them to functions, and return them from functions. Understanding closures deeply is essential for idiomatic Rust, because they power the entire iterator API, threading model, and async ecosystem.
Basic Syntax
A closure uses pipes (|params|) around its parameters, followed by the body:
fn main() {
// No parameters, no return value
let say_hello = || println!("Hello, closure!");
say_hello();
// One parameter, inferred types
let add_one = |x| x + 1;
println!("3 + 1 = {}", add_one(3)); // 4
// Explicit types (rarely needed)
let multiply = |x: f64, y: f64| -> f64 { x * y };
println!("{}", multiply(2.5, 4.0)); // 10.0
// Multi-line body with braces
let clamp = |val: i32, min: i32, max: i32| {
if val < min { min }
else if val > max { max }
else { val }
};
println!("{}", clamp(150, 0, 100)); // 100
}
Unlike regular functions, closures can infer their parameter and return types from context. The compiler locks in the types on first use — you cannot call the same closure with different types.
let parse = |s: &str| s.parse::<i32>();
let result = parse("42"); // Ok(42)
// parse(99); // Error: expected &str, found integer
The Three Capture Modes
The most important aspect of closures is how they capture variables from the enclosing scope. Rust has three modes, represented by three traits: Fn, FnMut, and FnOnce. The compiler automatically chooses the least restrictive mode that satisfies how the closure uses the captured values.
Fn — Immutable Borrow
The closure only reads captured values. It can be called any number of times.
fn main() {
let prefix = String::from("Hello");
let greet = |name: &str| println!("{}, {}!", prefix, name);
greet("Alice");
greet("Bob");
// prefix is still accessible here — only borrowed
println!("prefix is still: {}", prefix);
}
FnMut — Mutable Borrow
The closure modifies a captured variable. It can be called multiple times, but requires the closure binding itself to be mut.
fn main() {
let mut count = 0;
let mut increment = || {
count += 1;
count
};
println!("{}", increment()); // 1
println!("{}", increment()); // 2
println!("{}", increment()); // 3
// count is not accessible here while the closure borrows it mutably
drop(increment);
println!("final count: {}", count); // 3
}
A common mistake is calling an FnMut closure while also trying to use the captured value:
let mut total = 0;
let mut add = |x: i32| total += x;
add(5);
add(10);
// println!("{}", total); // Error: cannot borrow while mutably borrowed by `add`
drop(add);
println!("{}", total); // OK: 15
FnOnce — Takes Ownership
The closure takes ownership of captured values. It can only be called once because the value is moved out.
fn main() {
let items = vec![1, 2, 3];
// `items` is moved into the closure
let consume = || {
let sum: i32 = items.into_iter().sum();
println!("Sum: {}", sum);
};
consume();
// consume(); // Error: use of moved value
}
The trait hierarchy is: Fn: FnMut: FnOnce. Every Fn is also FnMut and FnOnce. When accepting a closure as a parameter, use the least restrictive bound your code needs.
The move Keyword
move forces a closure to take ownership of all captured variables, regardless of how they’re used. This is required when the closure outlives the scope where the variables were defined — most commonly in threads and async code.
use std::thread;
fn main() {
let data = vec![1, 2, 3];
let message = String::from("result");
let handle = thread::spawn(move || {
// Both `data` and `message` are owned by this thread
println!("{}: {:?}", message, data);
});
// data and message are no longer accessible here
handle.join().unwrap();
}
Without move, the compiler rejects this because the spawned thread could outlive the current stack frame, leaving dangling references.
move with Clone for Sharing
When you need both the original scope and the closure to have the value, clone before moving:
use std::thread;
fn main() {
let config = String::from("debug_mode=true");
// Clone so both main and the thread have an owned copy
let config_clone = config.clone();
let handle = thread::spawn(move || {
println!("Thread config: {}", config_clone);
});
println!("Main config: {}", config); // Still valid
handle.join().unwrap();
}
Closures as Function Parameters
The standard library is built around accepting closures. Use the Fn, FnMut, or FnOnce trait bounds to accept closures as parameters.
// Accept any closure that takes i32 and returns i32
fn apply(value: i32, f: impl Fn(i32) -> i32) -> i32 {
f(value)
}
// Accept a mutating closure
fn apply_mut(value: &mut i32, mut f: impl FnMut(i32) -> i32) {
*value = f(*value);
}
// Accept a consuming closure (called exactly once)
fn apply_once<F: FnOnce() -> String>(f: F) -> String {
f()
}
fn main() {
println!("{}", apply(5, |x| x * 2)); // 10
println!("{}", apply(3, |x| x + 100)); // 103
let mut n = 10;
apply_mut(&mut n, |x| x * 3);
println!("{}", n); // 30
let name = String::from("Rust");
let result = apply_once(move || format!("Hello, {}!", name));
println!("{}", result); // Hello, Rust!
}
Storing Multiple Closures with Box<dyn Fn>
When you need to store different closures in a collection or struct, use a trait object:
struct Middleware {
handlers: Vec<Box<dyn Fn(&str) -> String>>,
}
impl Middleware {
fn new() -> Self {
Middleware { handlers: Vec::new() }
}
fn add<F: Fn(&str) -> String + 'static>(&mut self, f: F) {
self.handlers.push(Box::new(f));
}
fn run(&self, input: &str) -> Vec<String> {
self.handlers.iter().map(|f| f(input)).collect()
}
}
fn main() {
let mut m = Middleware::new();
m.add(|s| s.to_uppercase());
m.add(|s| format!("[{}]", s));
m.add(|s| s.chars().rev().collect());
for result in m.run("hello") {
println!("{}", result);
}
// HELLO
// [hello]
// olleh
}
Returning Closures from Functions
Closures have unique, anonymous types, so you must return them behind a pointer. Use impl Fn(...) for owned closures or Box<dyn Fn(...)> when the concrete type must be erased.
// Return an owned closure (zero cost, type inferred)
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
// Return a boxed closure (useful for trait objects)
fn make_greeter(greeting: &str) -> Box<dyn Fn(&str) -> String> {
let g = greeting.to_string();
Box::new(move |name| format!("{}, {}!", g, name))
}
fn main() {
let add5 = make_adder(5);
let add10 = make_adder(10);
println!("{}", add5(3)); // 8
println!("{}", add10(3)); // 13
let greet = make_greeter("Good morning");
println!("{}", greet("Alice")); // Good morning, Alice!
}
Closures and the Iterator API
Closures shine when chained with iterator adaptors. This is where most Rust code encounters them:
fn main() {
let logs = vec![
"[INFO] Server started",
"[ERROR] Connection refused",
"[WARN] Retry attempt 1",
"[ERROR] Timeout",
"[INFO] Request complete",
];
// Filter errors, extract message, count
let error_count = logs.iter()
.filter(|line| line.starts_with("[ERROR]"))
.count();
println!("Errors: {}", error_count); // 2
// Transform into structured data
let parsed: Vec<(&str, &str)> = logs.iter()
.filter_map(|line| {
let rest = line.strip_prefix('[')?.split_once("] ")?;
Some((rest.0, rest.1))
})
.collect();
for (level, msg) in &parsed {
println!("{:>6} | {}", level, msg);
}
}
Closures Capturing State in Iterators
Closures that capture mutable state are especially powerful with scan and fold:
fn main() {
let prices = vec![100.0f64, 105.0, 98.0, 110.0, 107.0];
// Running maximum using fold
let running_max = prices.iter().fold(Vec::new(), |mut acc, &p| {
let current_max = acc.last().copied().unwrap_or(f64::NEG_INFINITY).max(p);
acc.push(current_max);
acc
});
println!("{:?}", running_max); // [100.0, 105.0, 105.0, 110.0, 110.0]
// Moving average with scan
let window = 3;
let moving_avg: Vec<f64> = prices.windows(window)
.map(|w| w.iter().sum::<f64>() / window as f64)
.collect();
println!("{:?}", moving_avg); // [101.0, 104.33..., 105.66...]
}
Common Patterns and Pitfalls
Closure in a Loop — Capture Gotcha
A classic bug when capturing loop variables:
fn main() {
let mut actions: Vec<Box<dyn Fn()>> = Vec::new();
for i in 0..3 {
// `i` is copied into each closure because i32 is Copy
actions.push(Box::new(move || println!("{}", i)));
}
for action in &actions {
action(); // Prints 0, 1, 2 correctly
}
}
For non-Copy types, clone inside the loop:
let labels = vec!["a", "b", "c"];
let mut actions: Vec<Box<dyn Fn()>> = Vec::new();
for label in &labels {
let label = label.to_string(); // Clone before move
actions.push(Box::new(move || println!("{}", label)));
}
Avoiding Unnecessary Clones with References
When a closure only needs to read data and doesn’t outlive the current scope, borrow instead of clone:
fn process(items: &[i32], filter: impl Fn(&i32) -> bool) -> Vec<i32> {
items.iter().filter(|x| filter(x)).copied().collect()
}
fn main() {
let threshold = 5;
// Borrow threshold — no clone needed
let result = process(&[1, 3, 5, 7, 9], |&x| x > threshold);
println!("{:?}", result); // [7, 9]
}
Memoization with a Closure-Wrapping Struct
A practical pattern for caching expensive closure results:
struct Memoized<T, F>
where
F: Fn(i32) -> T,
{
func: F,
cache: std::collections::HashMap<i32, T>,
}
impl<T: Clone, F: Fn(i32) -> T> Memoized<T, F> {
fn new(func: F) -> Self {
Memoized { func, cache: std::collections::HashMap::new() }
}
fn call(&mut self, arg: i32) -> T {
if let Some(cached) = self.cache.get(&arg) {
return cached.clone();
}
let result = (self.func)(arg);
self.cache.insert(arg, result.clone());
result
}
}
fn main() {
let mut expensive = Memoized::new(|n: i32| {
println!("Computing for {}...", n);
n * n
});
println!("{}", expensive.call(4)); // Computing for 4... → 16
println!("{}", expensive.call(4)); // Cached → 16
println!("{}", expensive.call(7)); // Computing for 7... → 49
}
Summary
| Trait | Capture mode | Callable | Use when |
|---|---|---|---|
FnOnce |
Takes ownership | Once | Closure moves out a value |
FnMut |
Mutable borrow | Many times | Closure modifies captured state |
Fn |
Immutable borrow | Many times | Closure only reads captured state |
Key rules to remember:
- Use
movewhen the closure must outlive the scope that created it (threads, async tasks, returning closures) - Accept
impl Fn(...)in function signatures to avoid unnecessary boxing - Use
Box<dyn Fn(...)>when you need to store heterogeneous closures or erase the concrete type - The compiler chooses the least restrictive trait automatically — you only need to specify bounds when writing generic code
Comments