Closures
A Rust closure is an anonymous function that can capture variables from its enclosing scope. Three flavours by capture mode: Fn (borrows), FnMut (mutably borrows), FnOnce (takes ownership).
Fn, FnMut, FnOnce, move, iterators
EXAMPLE
// 1) Basic closure syntax
let square = |x: i32| x * x;
println!("{}", square(5)); // 25
// Type inference — explicit annotations optional
let sum = |a, b| a + b;
let result = sum(3, 4); // i32, inferred
// Multi-line body — use braces
let sum_with_log = |a: i32, b: i32| -> i32 {
println!("adding {} + {}", a, b);
a + b
};
// 2) Capturing variables
let x = 10;
let add_x = |n| n + x; // captures x by reference
add_x(5); // 15
let mut count = 0;
let mut increment = || count += 1; // captures &mut
increment();
increment();
println!("{}", count); // 2
// 3) move keyword — take ownership
let name = String::from("Ada");
let greet = move || println!("Hi, {}", name); // name MOVED into closure
// name no longer usable here
greet();
// Useful for threads (closure outlives the original scope)
use std::thread;
let data = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("{:?}", data); // ownership moved into thread
});
handle.join().unwrap();
// 4) Closure traits — Fn, FnMut, FnOnce
// Fn: takes &self → can be called multiple times, doesn't mutate captures
fn call_n_times<F: Fn()>(f: F, n: u32) {
for _ in 0..n {
f();
}
}
let greet = || println!("hello");
call_n_times(greet, 3);
// FnMut: takes &mut self → can mutate captures, multiple calls OK
fn run_mut<F: FnMut()>(mut f: F) {
f();
f();
}
let mut counter = 0;
run_mut(|| counter += 1);
println!("{}", counter);
// FnOnce: takes self → can be called AT MOST ONCE (consumes captures)
fn run_once<F: FnOnce() -> String>(f: F) -> String {
f()
}
let name = String::from("Ada");
let greet = move || name; // moves name; can only call once
run_once(greet);
// 5) Closures as iterator combinators
let nums = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = nums.iter().map(|n| n * 2).collect();
let evens: Vec<i32> = nums.iter().filter(|&&n| n % 2 == 0).copied().collect();
let sum: i32 = nums.iter().sum();
let max: Option<i32> = nums.iter().copied().max();
let found: Option<&i32> = nums.iter().find(|&&n| n > 3);
// 6) Higher-order — return closures from functions
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y // move x into the closure
}
let add_5 = make_adder(5);
println!("{}", add_5(10)); // 15
// 7) Box<dyn Fn> — when impl Fn isn't enough (e.g. branching)
fn pick(op: &str) -> Box<dyn Fn(i32, i32) -> i32> {
match op {
"add" => Box::new(|a, b| a + b),
"sub" => Box::new(|a, b| a - b),
_ => Box::new(|a, b| 0),
}
}
let f = pick("add");
println!("{}", f(3, 4)); // 7
// 8) Storing closures in structs
struct Worker {
on_done: Box<dyn Fn(String)>,
}
impl Worker {
fn process(&self, data: String) {
// ... work ...
(self.on_done)(data);
}
}
let w = Worker {
on_done: Box::new(|s| println!("done: {}", s)),
};
w.process("hello".to_string());
// 9) Capture-by-reference vs by-move (decided automatically)
let data = vec![1, 2, 3];
let print_len = || println!("len: {}", data.len()); // captures &data
print_len();
print_len(); // still works
println!("{:?}", data); // still usable
let take = move || println!("len: {}", data.len()); // captures data by value
take();
// data is no longer accessible
// 10) Sharing state with Arc + Mutex
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
}));
}
for h in handles { h.join().unwrap(); }
println!("{}", *counter.lock().unwrap()); // 10
// 11) Closures + lifetimes
fn run<'a, F: Fn(&'a str) -> &'a str>(input: &'a str, f: F) -> &'a str {
f(input)
}
let upper = |s: &str| s;
run("hello", upper);
// 12) Async closures (stable in Rust 1.85)
async fn run<F, Fut>(f: F) -> i32
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = i32>,
{
f().await
}
// Or use the explicit async block:
let f = || async { 42 };
run(f).await;
// 13) Function pointers vs closures
fn double(x: i32) -> i32 { x * 2 }
let f: fn(i32) -> i32 = double; // fn — function pointer
let g = |x| x * 2; // closure — can capture
// Function pointers can be passed where Fn is expected
let doubled: Vec<i32> = vec![1, 2, 3].iter().map(|x| double(*x)).collect();
// 14) Patterns in callbacks
fn for_each<T, F: FnMut(&T)>(items: &[T], mut f: F) {
for item in items {
f(item);
}
}
for_each(&[1, 2, 3], |x| println!("{}", x));
let mut sum = 0;
for_each(&[1, 2, 3], |x| sum += x); // FnMut — captures sum
println!("{}", sum); // 6
// 15) Common patterns from the standard library
// thread::spawn
std::thread::spawn(move || {
// body
});
// std::sync::Once
static INIT: std::sync::Once = std::sync::Once::new();
INIT.call_once(|| {
// init code
});
// std::iter::repeat_with
std::iter::repeat_with(|| rand::random::<u32>())
.take(5)
.collect::<Vec<_>>();
// Option::map / Option::unwrap_or_else
let x: Option<i32> = Some(5);
let doubled = x.map(|n| n * 2);
let default = x.unwrap_or_else(|| compute_default());
// 16) Common bugs
// • Borrow checker errors → use move or restructure
// • Closure outlives borrowed value → use move or Arc
// • Calling FnOnce twice → compile error
// • Returning closure that captures local → use impl Fn or Box<dyn Fn>
// • Shared mutable state → use Mutex/RwLock; raw &mut won't work across closures
// 17) Performance
// - Closures monomorphise — zero overhead vs hand-written struct + method
// - Box<dyn Fn> has one vtable indirection (~1ns)
// - impl Trait return — preferred for single concrete type, avoids the Box
// - Inline closures in iterator chains are optimised aggressively
// 18) Best practices
// ✅ Let the compiler infer capture mode (it picks the most restrictive)
// ✅ Use move only when you need to (clarifies intent)
// ✅ Prefer impl Fn over Box<dyn Fn> when types unify
// ✅ Pair with iterators for declarative pipelines
// ✅ For shared mutable state, use Arc<Mutex<T>>
// ✅ For async, use async move || { ... } closures
Why it matters
Closures + iterators give Rust its zero-cost expressive power. Let the compiler pick capture mode (Fn/FnMut/FnOnce); reach for move when sending closures to threads, and Arc<Mutex<T>> for shared mutable state.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
let add = |a, b| a + b;
println!("{}", add(2, 3));
let n = 10;
let add_n = move |x| x + n;
println!("{}", add_n(5));
Try it Yourself »
Discussion
Loading…