ZetCode

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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
#[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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Rust tutorials.