Rust · lesson 19 of 30
Beginner
15- 1 Introduction to Rust
- 2 Setting Up Rust
- 3 Variables in Rust
- 4 Data Types in Rust
- 5 Operators in Rust
- 6 Control Flow in Rust
- 7 Functions in Rust
- 8 Ownership in Rust
- 9 Borrowing and References
- 11 Structs in Rust
- 12 Enums in Rust
- 13 Pattern Matching in Rust
- 17 Collections in Rust
- 25 Testing in Rust
- 26 File I/O in Rust
Intermediate
9Advanced
6Closures in Rust
Learn Rust closures through runnable examples: Fn, FnMut and FnOnce, environment capture, move closures, returning closures, and caching.
By SkillByExample · Updated
Closure Syntax
Closures are anonymous functions you can define inline and pass around as values. Unlike regular functions, they can capture variables from the surrounding scope, which makes them ideal for callbacks, iterators, and event handlers. The compiler infers parameter and return types from context, so you rarely need annotations.
fn main() {
// Annotated as a function pointer — only possible because it captures nothing
let add: fn(i32, i32) -> i32 = |a: i32, b: i32| -> i32 { a + b };
// Types inferred from usage
let multiply = |a, b| a * b;
// Single-expression body — no braces needed
let square = |x: i64| x * x;
// Multi-line body with braces
let clamp = |x: f64, lo: f64, hi: f64| {
if x < lo { lo }
else if x > hi { hi }
else { x }
};
println!("{}", add(3, 4));
println!("{}", multiply(3_i32, 4));
println!("{}", square(9));
println!("{}", clamp(15.0, 0.0, 10.0));
}
$ rustc closures_closure_syntax.rs && ./closures_closure_syntax
7
12
81
10
Capturing the Environment
The defining feature of closures over regular functions is environment capture — a closure can read and write variables from the scope it was created in. The compiler automatically chooses the least restrictive capture mode: it borrows immutably if it can, mutably if it must, and moves ownership only as a last resort. This keeps the original variables usable whenever possible.
fn main() {
let factor = 3;
let base = 100;
// Captures factor and base by immutable reference — both remain usable
let compute = |x| base + x * factor;
println!("{}", compute(5));
println!("{}", compute(10));
println!("{}", factor); // still accessible
}
$ rustc closures_capturing_the_environment.rs && ./closures_capturing_the_environment
115
130
3
The compiler’s capture priority:
| Priority | Mode | Trait |
|---|---|---|
| 1 (prefer) | Immutable borrow &T | Fn |
| 2 | Mutable borrow &mut T | FnMut |
| 3 (last resort) | Move (take ownership) | FnOnce |
The Three Closure Traits
FnOnce — Called At Most Once
FnOnce is the most permissive bound — it only requires that the closure can be called once. A closure that moves a captured value out of itself implements only FnOnce, because after that move the value is gone and calling the closure again would be undefined behavior. The compiler enforces the “at most once” rule at compile time.
fn consume_string<F: FnOnce() -> String>(f: F) {
let result = f();
println!("{}", result);
}
fn main() {
let greeting = String::from("Hello, Rust!");
// This closure moves greeting out when called — it can only be called once
let f = || greeting;
consume_string(f);
}
$ rustc closures_fnonce_called_at_most_once.rs && ./closures_fnonce_called_at_most_once
Hello, Rust!
Call it twice and the compiler stops you — not at runtime, but by type:
fn main() {
let greeting = String::from("Hello, Rust!");
let f = || greeting;
println!("{}", f());
println!("{}", f());
}
$ rustc closures_fnonce_called_at_most_once_2.rs
error[E0382]: use of moved value: `f`
--> closures_fnonce_called_at_most_once_2.rs:5:20
|
4 | println!("{}", f());
| --- `f` moved due to this call
5 | println!("{}", f());
| ^ value used here after move
|
note: closure cannot be invoked more than once because it moves the variable `greeting` out of its environment
--> closures_fnonce_called_at_most_once_2.rs:3:16
|
3 | let f = || greeting;
| ^^^^^^^^
note: this value implements `FnOnce`, which causes it to be moved when called
--> closures_fnonce_called_at_most_once_2.rs:4:20
|
4 | println!("{}", f());
| ^
The note explains why f is FnOnce: its body moves greeting out, so there is nothing left to return on a second call.
FnMut — Called Multiple Times, Mutates Captured State
FnMut allows repeated calls while letting the closure mutate captured variables. Accumulation patterns — incrementing a counter, building a string, collecting results — use FnMut. The caller must declare the binding mut to signal that calling the closure changes state.
fn apply_n_times<F: FnMut()>(mut f: F, n: usize) {
for _ in 0..n {
f();
}
}
fn main() {
let mut count = 0;
// Mutably captures count — must be called through a mut binding
apply_n_times(|| {
count += 1;
println!("call #{}", count);
}, 3);
println!("final count: {}", count);
}
$ rustc closures_fnmut_called_multiple_times_mutates_capt.rs && ./closures_fnmut_called_multiple_times_mutates_capt
call #1
call #2
call #3
final count: 3
Fn — Called Multiple Times, No Mutation
Fn is the most restrictive bound and the most flexible to use. A closure that only reads captured values — never mutates, never consumes — implements Fn. They can be called any number of times through a shared reference and reused freely. Fn says nothing about threads, though: whether a closure can cross threads depends on what it captures — one that captures an Rc is Fn but neither Send nor Sync. When writing a function that takes a callback, reach for Fn if you don’t need mutation.
fn apply_to_vec<F: Fn(i32) -> i32>(v: &[i32], f: F) -> Vec<i32> {
v.iter().map(|&x| f(x)).collect()
}
fn main() {
let offset = 10;
// Immutably captures offset — can be called any number of times
let result = apply_to_vec(&[1, 2, 3, 4, 5], |x| x + offset);
println!("{:?}", result);
println!("{}", offset); // offset still usable
}
$ rustc closures_fn_called_multiple_times_no_mutation.rs && ./closures_fn_called_multiple_times_no_mutation
[11, 12, 13, 14, 15]
10
move Closures
move forces the closure to take ownership of all captured variables, regardless of whether ownership is strictly needed. This is required any time the closure must outlive the scope where the variables were defined — the most common case is spawning threads, where the closure runs on a different thread that may outlast the function that created it. Without move, the borrow would dangle.
use std::thread;
fn main() {
let data = vec![1, 2, 3];
// Without move, data is borrowed — but the thread could outlive main(),
// making the borrow dangle. move transfers ownership to the closure.
let handle = thread::spawn(move || {
println!("{:?}", data); // data is owned by this closure/thread
});
// println!("{:?}", data); // ERROR: data was moved into the closure
handle.join().unwrap();
}
$ rustc closures_move_closures.rs && ./closures_move_closures
[1, 2, 3]
Factory Functions with move
move closures are also how you build factory functions that return closures carrying their own state. Each call to the factory creates a new closure with its own copy of the captured value, so the closures are independent.
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n // n is moved into the closure; each call to make_adder gets a fresh n
}
fn make_multiplier(n: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |x| x * n)
}
fn main() {
let add5 = make_adder(5);
let add10 = make_adder(10);
let triple = make_multiplier(3);
println!("{}", add5(3));
println!("{}", add10(3));
println!("{}", triple(7));
// Closures compose naturally
let add5_then_triple = |x| triple(add5(x));
println!("{}", add5_then_triple(4)); // (4 + 5) * 3
}
$ rustc closures_factory_functions_with_move.rs && ./closures_factory_functions_with_move
8
13
21
27
Closures as Return Values
Returning a closure from a function requires naming its type. Use impl Fn(...) for static dispatch when there is one possible closure type, and Box<dyn Fn(...)> for dynamic dispatch when the closure returned depends on a runtime condition. The latter is slightly heavier due to heap allocation and vtable dispatch, but necessary when the return type genuinely varies.
// impl Fn — zero-cost, but must return the same closure shape every time
fn make_greeting(prefix: &str) -> impl Fn(&str) -> String + '_ {
move |name| format!("{}, {}!", prefix, name)
}
// Box<dyn Fn> — necessary when the returned closure differs per branch
fn make_transform(kind: &str) -> Box<dyn Fn(i32) -> i32> {
match kind {
"double" => Box::new(|x| x * 2),
"square" => Box::new(|x| x * x),
"negate" => Box::new(|x| -x),
_ => Box::new(|x| x),
}
}
fn main() {
let greet = make_greeting("Hello");
println!("{}", greet("Alice"));
let transform = make_transform("square");
println!("{}", transform(7));
}
$ rustc closures_closures_as_return_values.rs && ./closures_closures_as_return_values
Hello, Alice!
49
Storing Closures in Structs
Storing a closure in a struct is a common pattern for lazy evaluation, caching, and strategy objects. Using a generic parameter with a trait bound gives static dispatch; using Box<dyn Fn> gives runtime flexibility at the cost of a heap allocation per closure.
// Generic parameter — the closure type is baked in at compile time (zero overhead)
struct Cache<F: Fn(i32) -> i32> {
func: F,
cache: std::collections::HashMap<i32, i32>,
}
impl<F: Fn(i32) -> i32> Cache<F> {
fn new(func: F) -> Self {
Self { func, cache: std::collections::HashMap::new() }
}
fn call(&mut self, arg: i32) -> i32 {
// Compute once and memoize — subsequent calls return the cached result
*self.cache.entry(arg).or_insert_with(|| (self.func)(arg))
}
}
fn main() {
let mut cache = Cache::new(|x| {
println!("computing for {}", x);
x * x
});
println!("{}", cache.call(4));
println!("{}", cache.call(4)); // cached — no "computing" line
println!("{}", cache.call(5));
}
$ rustc --edition 2021 closures_storing_closures_in_structs.rs && ./closures_storing_closures_in_structs
computing for 4
16
16
computing for 5
25
The second call(4) prints only 16: the closure never ran. Note the --edition 2021 flag. or_insert_with(|| (self.func)(arg)) borrows self.cache mutably while the inner closure reads self.func — the 2021 edition lets a closure capture just the field it uses (self.func), so the two borrows don’t overlap. Under plain rustc (edition 2015) the closure captures all of self and the same code fails with E0502. Cargo projects default to edition 2021 or later, so this only bites when compiling a single file by hand.
Closure Type Inference Rules
Every closure has a unique anonymous type generated by the compiler — two closures with identical bodies are still different types. To put several in one Vec you need a common type: non-capturing closures coerce to plain fn pointers, and closures that capture variables need Box<dyn Fn>.
fn main() {
// These two closures have different types even though they look the same
let f = |x: i32| x + 1;
let g = |x: i32| x + 1;
// f and g have different types, but neither captures anything,
// so vec![f, g] would compile by coercing both to fn(i32) -> i32
println!("f(1) = {}, g(1) = {}", f(1), g(1));
// Non-capturing closures coerce to function pointers
let ops: Vec<fn(i32) -> i32> = vec![
|x| x + 1,
|x| x * 2,
|x| x * x,
];
for op in &ops {
print!("{} ", op(3));
}
println!();
// Capturing closures require Box<dyn Fn>
let base = 100;
let ops2: Vec<Box<dyn Fn(i32) -> i32>> = vec![
Box::new(|x| x + 1),
Box::new(move |x| x + base),
];
for op in &ops2 {
print!("{} ", op(5));
}
println!();
}
$ rustc closures_closure_type_inference_rules.rs && ./closures_closure_type_inference_rules
f(1) = 2, g(1) = 2
4 6 9
6 105
Practical: Event Dispatcher
A common real-world use of closures is an event system where multiple handlers can subscribe to an event. Each handler is a closure that captures its own state, enabling a flexible plugin-style architecture without inheritance or virtual dispatch overhead beyond what Box<dyn Fn> already implies.
struct EventBus {
handlers: Vec<Box<dyn Fn(&str)>>,
}
impl EventBus {
fn new() -> Self { Self { handlers: Vec::new() } }
// Accept any closure that takes a &str, owns its captures, and lives for 'static
fn on(&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 bus = EventBus::new();
bus.on(|e| println!("[Logger] event: {}", e));
let prefix = "APP";
bus.on(move |e| println!("[{}] received: {}", prefix, e));
bus.emit("user_login");
bus.emit("page_view");
}
$ rustc closures_practical_event_dispatcher.rs && ./closures_practical_event_dispatcher
[Logger] event: user_login
[APP] received: user_login
[Logger] event: page_view
[APP] received: page_view
Practice
-
Add
let n = 1;and make bothfandgcapture it:let f = move |x: i32| x + n;(same forg), then writelet v = vec![f, g];. Does it compile? What if you drop thencapture?Solution
With the capture, no:
error[E0308]: mismatched types ... = note: no two closures, even if identical, have the same type = help: consider boxing your closure and/or using it as a trait objectWithout any capture,
vec![|x: i32| x + 1, |x: i32| x * 10]compiles — both coerce tofn(i32) -> i32— and calling each with5prints6and50. -
Compile the
Cacheexample with plainrustc(no--editionflag). What error do you get, and which line of the lesson explains it?Solution
error[E0502]: cannot borrow `self` as immutable because it is also borrowed as mutable --> closures_cache2015.rs:12:47 | 12 | *self.cache.entry(arg).or_insert_with(|| (self.func)(arg)) | ---------- -------------- ^^ ----------- second borrow occurs due to use of `self` in closureEdition 2015 closures capture whole variables, so
|| (self.func)(arg)borrows all ofselfwhileself.cacheis already mutably borrowed. With--edition 2021the closure captures onlyself.func, a different field. -
Change
apply_n_times’s bound fromFnMut()toFn()(and drop themutonf). What does the compiler suggest?Solution
error[E0594]: cannot assign to `count`, as it is a captured variable in a `Fn` closureIt points at the bound itself:
change this to accept \FnMut` instead of `Fn`. A closure that assigns to a captured variable isFnMut, and anFn` bound promises the callee can call it through a shared reference, which would make the mutation unsound. -
Write
compose(f, g)that returns a closure computingg(f(x)), and use it to buildshoutfrom|s: &str| s.to_uppercase()and|s: String| format!("{}!", s).Solution
fn compose<A, B, C>(f: impl Fn(A) -> B, g: impl Fn(B) -> C) -> impl Fn(A) -> C { move |x| g(f(x)) }shout("hello")returnsHELLO!.moveis required: the returned closure ownsfandg, which would otherwise be dropped whencomposereturns.