Rust closures
last modified October 25, 2026
In this article we show how to work with closures in Rust.
A closure is an anonymous function that can capture values from
the scope in which it is defined. Closures are written with vertical bars
around the parameters: |x| x + 1. They are used everywhere in
Rust: as arguments to iterator adapters, as callbacks, as sorting keys, and
as the body of new threads.
Every closure has its own unique, anonymous type. Functions that work with
closures use one of three traits: Fn, FnMut, or
FnOnce. Which traits a closure implements depends on what it
does with the values it captures. Plain functions are covered in
Rust functions.
The examples were tested with Rust 1.97 and the 2024 edition. They are run as
Cargo projects with cargo run -q.
Closure syntax
Closures have a lighter syntax than functions. Types can usually be left out, because the compiler infers them from how the closure is used.
fn add_one_fn(x: i32) -> i32 {
x + 1
}
fn main() {
let add_one_v1 = |x: i32| -> i32 { x + 1 };
let add_one_v2 = |x: i32| x + 1;
let add_one_v3 = |x| x + 1;
println!("{}", add_one_fn(5));
println!("{}", add_one_v1(5));
println!("{}", add_one_v2(5));
println!("{}", add_one_v3(5));
let greet = || println!("Hello there!");
greet();
let area = |w: f64, h: f64| {
let a = w * h;
a.round()
};
println!("{}", area(3.4, 5.5));
}
We define a function and several equivalent closures.
let add_one_v1 = |x: i32| -> i32 { x + 1 };
let add_one_v2 = |x: i32| x + 1;
let add_one_v3 = |x| x + 1;
The first closure is fully annotated, much like a function. The second omits
the return type; with a single expression the braces can be dropped too. The
third omits all types. The compiler infers i32 from the first
call. Once inferred, the types are fixed: calling add_one_v3
later with a f64 would be an error.
let greet = || println!("Hello there!");
A closure without parameters has an empty pair of bars.
let area = |w: f64, h: f64| {
let a = w * h;
a.round()
};
A closure body with several statements is placed in a block. The last expression is the return value.
$ cargo run -q 6 6 6 6 Hello there! 19
Capturing by reference
Unlike functions, closures can use variables from the surrounding scope.
fn main() {
let rate = 0.2;
let tax = |price: f64| price * rate;
println!("{:.2}", tax(100.0));
println!("{:.2}", tax(45.5));
println!("rate is still usable: {rate}");
let names = vec!["Peter", "Lucy"];
let show = || println!("names: {names:?}");
show();
println!("len: {}", names.len());
show();
}
By default a closure captures variables in the least restrictive way it can. If it only reads a variable, it borrows it immutably.
let rate = 0.2; let tax = |price: f64| price * rate;
The tax closure reads rate from its environment. A
nested fn item could not do this; it would have to get the rate
as a parameter.
let show = || println!("names: {names:?}");
show();
println!("len: {}", names.len());
show();
The closure holds a shared reference to names. Other shared
borrows are allowed at the same time, so we can read the vector between the
calls.
$ cargo run -q 20.00 9.10 rate is still usable: 0.2 names: ["Peter", "Lucy"] len: 2 names: ["Peter", "Lucy"]
Capturing by mutable reference
A closure that modifies a captured variable borrows it mutably.
fn main() {
let mut count = 0;
let mut inc = || {
count += 1;
println!("count: {count}");
};
inc();
inc();
inc();
println!("final: {count}");
let mut log = Vec::new();
let mut record = |msg: &str| log.push(msg.to_uppercase());
record("start");
record("stop");
println!("{log:?}");
}
The closure increments a counter each time it is called.
let mut inc = || {
count += 1;
println!("count: {count}");
};
Calling the closure changes its captured state, so the closure variable
itself must be declared with mut. While inc is in
use, count is mutably borrowed and cannot be read elsewhere.
println!("final: {count}");
After the last call of inc, the mutable borrow ends and we can
read count again.
let mut record = |msg: &str| log.push(msg.to_uppercase());
The second closure pushes messages into a captured vector.
$ cargo run -q count: 1 count: 2 count: 3 final: 3 ["START", "STOP"]
move closures
The move keyword forces a closure to take ownership of the
captured variables.
use std::thread;
fn main() {
let data: Vec<i32> = (1..=100).collect();
let handle = thread::spawn(move || {
let sum: i32 = data.iter().sum();
println!("sum computed in thread: {sum}");
});
handle.join().unwrap();
let x = 10;
let print_x = move || println!("x: {x}");
print_x();
println!("x is Copy, still available: {x}");
let name = String::from("Robert");
let greet = move || println!("Hello, {name}");
greet();
greet();
}
A move closure is needed when the closure may outlive the
current scope, typically when it is passed to a new thread.
let handle = thread::spawn(move || {
let sum: i32 = data.iter().sum();
println!("sum computed in thread: {sum}");
});
The closure only reads data, so without move it
would borrow it. The compiler rejects that, because the new thread might run
longer than main. With move, the vector is moved
into the closure and the thread owns it.
let x = 10;
let print_x = move || println!("x: {x}");
print_x();
println!("x is Copy, still available: {x}");
For Copy types, move copies the value. The
original variable is still usable.
let name = String::from("Robert");
let greet = move || println!("Hello, {name}");
greet();
greet();
The name string is moved into the closure and can no longer be
used in main. The closure itself can be called many times;
move decides how variables are captured, not how often the
closure can run.
$ cargo run -q sum computed in thread: 5050 x: 10 x is Copy, still available: 10 Hello, Robert Hello, Robert
FnOnce closures
A closure that moves a captured value out of its body can be called only once.
fn main() {
let msg = String::from("a message");
let consume = || {
let owned = msg;
println!("consumed: {owned}");
};
consume();
let parts = String::from("a b c");
let joiner = move || parts.replace(' ', "+");
println!("{}", joiner());
println!("{}", joiner());
}
The first closure gives away its captured string.
let consume = || {
let owned = msg;
println!("consumed: {owned}");
};
The body moves msg into a local variable. After the first call,
the closure no longer has the string, so a second call is impossible. Such a
closure implements only FnOnce.
let parts = String::from("a b c");
let joiner = move || parts.replace(' ', "+");
The second closure owns its string as well, but it only reads it. It can be called repeatedly.
$ cargo run -q consumed: a message a+b+c a+b+c
Calling an FnOnce closure twice
The following program calls an FnOnce closure twice.
fn main() {
let msg = String::from("a message");
let consume = move || {
let owned = msg;
println!("consumed: {owned}");
};
consume();
consume();
}
The compiler rejects the second call and explains which captured variable is the cause.
$ cargo run -q error[E0382]: use of moved value: `consume` --> src/main.rs:10:5 | 9 | consume(); | --------- `consume` moved due to this call 10 | consume(); | ^^^^^^^ value used here after move | note: closure cannot be invoked more than once because it moves the variable `msg` out of its environment --> src/main.rs:5:21 | 5 | let owned = msg; | ^^^ note: this value implements `FnOnce`, which causes it to be moved when called --> src/main.rs:9:5 | 9 | consume(); | ^^^^^^^ For more information about this error, try `rustc --explain E0382`.
Fn, FnMut, and FnOnce
The three traits form a hierarchy. Every closure implements
FnOnce; those that do not move captured values out also
implement FnMut; those that do not mutate captured values also
implement Fn.
fn apply<F: Fn(i32) -> i32>(f: F, val: i32) -> i32 {
f(val)
}
fn call_twice(mut f: impl FnMut()) {
f();
f();
}
fn run_once<F>(f: F) -> String
where
F: FnOnce() -> String,
{
f()
}
fn main() {
let offset = 7;
println!("{}", apply(|x| x * 3, 5));
println!("{}", apply(|x| x + offset, 5));
let mut n = 0;
call_twice(|| n += 10);
println!("n: {n}");
let s = String::from("owned text");
let res = run_once(move || s + "!");
println!("{res}");
println!("{}", apply(|x| x - 1, 0));
println!("{}", run_once(|| String::from("Fn closures are FnOnce too")));
}
Functions that take closures are generic over the closure type and use one of the traits as a bound.
fn apply<F: Fn(i32) -> i32>(f: F, val: i32) -> i32 {
f(val)
}
The Fn(i32) -> i32 bound accepts any closure that takes an
i32, returns an i32, and can be called through a
shared reference. Both a capture-free closure and one capturing
offset qualify.
fn call_twice(mut f: impl FnMut()) {
f();
f();
}
An FnMut parameter can be called many times, and the closure may
change its captured state. The parameter must be declared mut.
The impl Trait syntax is a shorter form of a generic parameter.
fn run_once<F>(f: F) -> String
where
F: FnOnce() -> String,
{
f()
}
An FnOnce bound is the least demanding for the caller: any
closure fits. The function may call it only once. The closure passed here
consumes its captured string with s + "!".
When writing a function, choose the most permissive bound that the function
body allows: FnOnce if the closure is called once,
FnMut if it is called repeatedly, and Fn if it
must be called through a shared reference, for example from several
threads.
$ cargo run -q 15 12 n: 20 owned text! -1 Fn closures are FnOnce too
Returning closures
Functions can create and return closures.
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
fn make_counter() -> impl FnMut() -> u32 {
let mut count = 0;
move || {
count += 1;
count
}
}
fn make_op(name: &str) -> Box<dyn Fn(i32, i32) -> i32> {
match name {
"add" => Box::new(|a, b| a + b),
"mul" => Box::new(|a, b| a * b),
_ => Box::new(|_, _| 0),
}
}
fn main() {
let add5 = make_adder(5);
let add10 = make_adder(10);
println!("{} {}", add5(1), add10(1));
let mut c1 = make_counter();
let mut c2 = make_counter();
println!("{} {} {}", c1(), c1(), c1());
println!("{}", c2());
for op in ["add", "mul", "pow"] {
let f = make_op(op);
println!("{op}: {}", f(3, 4));
}
}
The example builds closures in factory functions.
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
The return type impl Fn(i32) -> i32 means "some closure type
with this signature". The move keyword is required: the
closure must own n, because the parameter is dropped when the
function returns.
fn make_counter() -> impl FnMut() -> u32 {
let mut count = 0;
move || {
count += 1;
count
}
}
Each counter owns its own copy of count. The two counters in
main are independent.
fn make_op(name: &str) -> Box<dyn Fn(i32, i32) -> i32> {
match name {
"add" => Box::new(|a, b| a + b),
"mul" => Box::new(|a, b| a * b),
_ => Box::new(|_, _| 0),
}
}
An impl Fn return type stands for a single concrete type. The
three closures in the match have three different types, so we
box them and return a trait object Box<dyn Fn>.
$ cargo run -q 6 11 1 2 3 1 add: 7 mul: 12 pow: 0
Storing closures in structs
A struct field can hold a closure, either as a generic type parameter or as a boxed trait object.
struct Button<F>
where
F: Fn(&str),
{
label: String,
on_click: F,
}
impl<F: Fn(&str)> Button<F> {
fn click(&self) {
(self.on_click)(&self.label);
}
}
type Rule = Box<dyn Fn(&str) -> bool>;
struct Validator {
rules: Vec<(&'static str, Rule)>,
}
impl Validator {
fn check(&self, input: &str) {
for (name, rule) in &self.rules {
let res = if rule(input) { "ok" } else { "failed" };
println!(" {name}: {res}");
}
}
}
fn main() {
let btn = Button {
label: String::from("Save"),
on_click: |label| println!("{label} clicked"),
};
btn.click();
let min_len = 8;
let v = Validator {
rules: vec![
("length", Box::new(move |s| s.len() >= min_len)),
("digit", Box::new(|s| s.chars().any(|c| c.is_ascii_digit()))),
("upper", Box::new(|s| s.chars().any(|c| c.is_uppercase()))),
],
};
for pwd in ["secret", "Secret2026"] {
println!("{pwd}:");
v.check(pwd);
}
}
The Button struct stores one callback and the
Validator stores a list of rules.
struct Button<F>
where
F: Fn(&str),
{
label: String,
on_click: F,
}
The generic parameter F is the type of the closure. Each
button with a different closure is a different type, and the call is
dispatched statically.
(self.on_click)(&self.label);
To call a closure stored in a field, we wrap the field access in
parentheses. Without them, Rust would look for a method named
on_click.
type Rule = Box<dyn Fn(&str) -> bool>;
struct Validator {
rules: Vec<(&'static str, Rule)>,
}
A vector needs elements of one type. Boxing turns the different closures
into the same type, Box<dyn Fn(&str) -> bool>. The
type alias keeps the declaration readable.
("length", Box::new(move |s| s.len() >= min_len)),
The first rule captures min_len. A boxed closure stored in a
struct must not borrow local variables unless the struct carries a lifetime,
so we move the value in.
$ cargo run -q Save clicked secret: length: failed digit: failed upper: failed Secret2026: length: ok digit: ok upper: ok
Closures and iterators
Closures are most often used as arguments to iterator adapters and collection methods.
#[derive(Debug)]
struct Task {
name: &'static str,
priority: u8,
done: bool,
}
fn main() {
let mut tasks = vec![
Task { name: "write report", priority: 2, done: false },
Task { name: "fix bug", priority: 1, done: true },
Task { name: "review code", priority: 3, done: false },
Task { name: "deploy", priority: 1, done: false },
];
tasks.sort_by_key(|t| t.priority);
let names: Vec<_> = tasks.iter().map(|t| t.name).collect();
println!("{names:?}");
let limit = 2;
let urgent = tasks.iter().filter(|t| t.priority <= limit && !t.done).count();
println!("urgent and open: {urgent}");
tasks.retain(|t| !t.done);
println!("open tasks: {}", tasks.len());
let fallback = tasks.iter().find(|t| t.priority > 5).map_or("none", |t| t.name);
println!("high priority: {fallback}");
}
We process a list of tasks.
tasks.sort_by_key(|t| t.priority);
The closure extracts the sorting key. The sort is stable, so tasks with the same priority keep their original order.
let limit = 2; let urgent = tasks.iter().filter(|t| t.priority <= limit && !t.done).count();
The predicate captures the local limit variable.
tasks.retain(|t| !t.done);
The retain method keeps only the elements for which the closure
returns true.
let fallback = tasks.iter().find(|t| t.priority > 5).map_or("none", |t| t.name);
The map_or method of Option applies the closure to
a found value or returns the default. More iterator adapters are covered in
Rust iterators.
$ cargo run -q ["fix bug", "deploy", "write report", "review code"] urgent and open: 2 open tasks: 3 high priority: none
Function pointers
The fn type is a pointer to a function. Closures that capture
nothing can be converted to it.
type Op = fn(i32) -> i32;
fn double(x: i32) -> i32 {
x * 2
}
fn apply_ptr(f: Op, val: i32) -> i32 {
f(val)
}
fn main() {
println!("{}", apply_ptr(double, 4));
println!("{}", apply_ptr(|x| x - 1, 4));
let ops: [(&str, Op); 3] = [
("double", double),
("square", |x| x * x),
("negate", |x| -x),
];
for (name, f) in ops {
println!("{name}: {}", f(6));
}
let words = ["sky", "blue", "cloud"];
let lens: Vec<usize> = words.iter().map(|w| w.len()).collect();
let upper: Vec<String> = words.iter().map(|w| w.to_uppercase()).collect();
let owned: Vec<String> = words.into_iter().map(String::from).collect();
println!("{lens:?} {upper:?} {owned:?}");
}
The apply_ptr function takes a function pointer instead of a
generic closure.
type Op = fn(i32) -> i32;
Note the lowercase fn: this is a concrete type, not a trait.
All functions and capture-free closures with this signature share it.
let ops: [(&str, Op); 3] = [
("double", double),
("square", |x| x * x),
("negate", |x| -x),
];
Because they have a common type, a named function and two closures fit into one array without boxing. A closure that captured a variable could not be stored here.
let owned: Vec<String> = words.into_iter().map(String::from).collect();
Function pointers implement all three closure traits, so a function name can
be passed where a closure is expected. String::from replaces
the closure |w| String::from(w).
$ cargo run -q 8 3 double: 12 square: 36 negate: -6 [3, 4, 5] ["SKY", "BLUE", "CLOUD"] ["sky", "blue", "cloud"]
Capturing struct fields
Since the 2021 edition, closures capture only the parts of a struct they actually use.
struct Player {
name: String,
score: u32,
}
fn main() {
let mut p = Player {
name: String::from("Alice"),
score: 0,
};
let mut add_points = |n: u32| p.score += n;
println!("player: {}", p.name);
add_points(10);
add_points(5);
println!("{} has {} points", p.name, p.score);
}
The closure modifies the score field of a player.
let mut add_points = |n: u32| p.score += n;
println!("player: {}", p.name);
add_points(10);
The closure mutably borrows only p.score, not the whole
p. Therefore we can read p.name while the closure
is still alive. In the 2018 edition the closure captured the entire struct,
and this program did not compile.
$ cargo run -q player: Alice Alice has 15 points
Closure size
A closure is a compiler-generated struct whose fields are the captured values. We can inspect its size.
use std::mem::size_of_val;
fn main() {
let a = 1u8;
let b = 2u64;
let s = String::from("hello");
let no_capture = || 42;
let by_ref = || a as u64 + b;
let by_value = move || a as u64 + b;
let owns_string = move || s.len();
println!("no capture: {} bytes", size_of_val(&no_capture));
println!("by reference: {} bytes", size_of_val(&by_ref));
println!("by value: {} bytes", size_of_val(&by_value));
println!("owns String: {} bytes", size_of_val(&owns_string));
println!("{} {} {} {}", no_capture(), by_ref(), by_value(), owns_string());
}
The size_of_val function returns the size of a value in bytes.
let no_capture = || 42;
A closure without captures has no fields; it is zero-sized.
let by_ref = || a as u64 + b; let by_value = move || a as u64 + b;
The first closure stores two references of eight bytes each. The second
stores a u8 and a u64; with alignment padding this
is also sixteen bytes.
let owns_string = move || s.len();
A closure that owns a String stores the string's pointer,
length, and capacity. No heap allocation is made for the closure itself;
closures are as cheap as the data they capture.
$ cargo run -q no capture: 0 bytes by reference: 16 bytes by value: 16 bytes owns String: 24 bytes 42 3 3 5
Lazy evaluation and caching
Closures let us delay a computation until its result is needed.
use std::collections::HashMap;
struct Memo<F: Fn(u64) -> u64> {
func: F,
cache: HashMap<u64, u64>,
}
impl<F: Fn(u64) -> u64> Memo<F> {
fn new(func: F) -> Self {
Memo { func, cache: HashMap::new() }
}
fn get(&mut self, arg: u64) -> u64 {
if let Some(&v) = self.cache.get(&arg) {
println!(" cache hit for {arg}");
return v;
}
let v = (self.func)(arg);
self.cache.insert(arg, v);
v
}
}
fn main() {
let mut slow_square = Memo::new(|x| {
println!(" computing {x} * {x}");
x * x
});
for n in [4, 7, 4, 4, 7] {
println!("{n} -> {}", slow_square.get(n));
}
let config: Option<&str> = None;
let port = config.map_or_else(
|| {
println!("using default port");
8080
},
|s| s.parse().unwrap_or(80),
);
println!("port: {port}");
}
The Memo struct wraps a function and remembers its results.
fn get(&mut self, arg: u64) -> u64 {
if let Some(&v) = self.cache.get(&arg) {
println!(" cache hit for {arg}");
return v;
}
let v = (self.func)(arg);
self.cache.insert(arg, v);
v
}
The stored closure runs only for arguments that are not cached yet. Repeated requests are answered from the hash map.
let port = config.map_or_else(
|| {
println!("using default port");
8080
},
|s| s.parse().unwrap_or(80),
);
The standard library uses closures for lazily computed defaults. The first
closure of map_or_else runs only when the option is
None. The _else methods such as
unwrap_or_else and ok_or_else follow the same idea:
the default is computed only when it is needed.
$ cargo run -q computing 4 * 4 4 -> 16 computing 7 * 7 7 -> 49 cache hit for 4 4 -> 16 cache hit for 4 4 -> 16 cache hit for 7 7 -> 49 using default port port: 8080
Source
The Rust Programming Language: Closures
The Rust Reference: Closure types
In this article we have worked with closures in Rust.
Author
List all Rust tutorials.