Skip to main content
Rust · lesson 19 of 30
Rust intermediate Lesson 19 of 30

Closures 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:

PriorityModeTrait
1 (prefer)Immutable borrow &TFn
2Mutable borrow &mut TFnMut
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

  1. Add let n = 1; and make both f and g capture it: let f = move |x: i32| x + n; (same for g), then write let v = vec![f, g];. Does it compile? What if you drop the n capture?

    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 object

    Without any capture, vec![|x: i32| x + 1, |x: i32| x * 10] compiles — both coerce to fn(i32) -> i32 — and calling each with 5 prints 6 and 50.

  2. Compile the Cache example with plain rustc (no --edition flag). 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 closure

    Edition 2015 closures capture whole variables, so || (self.func)(arg) borrows all of self while self.cache is already mutably borrowed. With --edition 2021 the closure captures only self.func, a different field.

  3. Change apply_n_times’s bound from FnMut() to Fn() (and drop the mut on f). What does the compiler suggest?

    Solution
    error[E0594]: cannot assign to `count`, as it is a captured variable in a `Fn` closure

    It points at the bound itself: change this to accept \FnMut` instead of `Fn`. A closure that assigns to a captured variable is FnMut, and an Fn` bound promises the callee can call it through a shared reference, which would make the mutation unsound.

  4. Write compose(f, g) that returns a closure computing g(f(x)), and use it to build shout from |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") returns HELLO!. move is required: the returned closure owns f and g, which would otherwise be dropped when compose returns.

Frequently Asked Questions

What is the difference between Fn, FnMut, and FnOnce?
FnOnce can be called once and may consume captured values. FnMut can be called multiple times and may mutate captured values. Fn can be called multiple times without mutating captured values. Every closure implements at least FnOnce; Fn is the most restrictive.
What does the move keyword do in a closure?
move forces the closure to take ownership of all captured variables rather than borrowing them. This is required when the closure must outlive the scope where the variables were defined, e.g. in threads.
What is the type of a closure?
Each closure has a unique anonymous type generated by the compiler. You can only name it through generic bounds (Fn/FnMut/FnOnce) or the impl Trait syntax.