Skip to content
rusthacks.dev
Rust

Closures in Rust and How Fn FnMut and FnOnce Differ

Closures capture their environment, and the Fn, FnMut and FnOnce traits describe how. See which one the compiler picks and which bound to write.

9 min read
Summarize in AI
Flat illustration for the post “Closures in Rust and How Fn FnMut and FnOnce Differ”, composed around iterators.

A closure is a function you can write in the middle of another function, and the thing that sets it apart from a plain fn is that it can use the variables around it. Rust's closures are fast, because each one is its own type and compiles to a small struct, and they come with three traits that look intimidating until you see the question each one answers: how does this closure use what it captured? This post shows each trait in a running program, the three errors that teach the rules, and what closures cost in bytes. Everything ran on Rust 1.99, edition 2024.

Capturing the environment

src/main.rsrust
fn main() {
    let factor = 3;
    let times = |n: i32| n * factor;           // captures `factor` by shared reference
    println!("{} {}", times(4), times(5));

    let mut count = 0;
    let mut bump = || {
        count += 1;                            // captures `count` by mutable reference
        count
    };
    println!("{} {}", bump(), bump());
    println!("count = {count}");               // fine: the closure is no longer used

    let name = String::from("ferris");
    let consume = move || name;                // captures `name` by value and returns it
    let back = consume();
    println!("{back}");
}
Text
12 15
1 2
count = 2
ferris

The first closure, times, uses factor without changing it, so it captures a shared reference and can be called as often as you like. The second, bump, changes count, so it captures a mutable reference and must be declared let mut bump. While bump is alive, nothing else may touch count, which is why println!("count = {count}") comes after the last call, once the borrow has ended. The third closure uses move, which makes it take ownership of name; it returns the String, so the closure hands its captured value out to whoever calls it. After move, name is no longer usable in main.

The compiler picks the weakest capture that the body allows. You do not annotate that; you write move only when you want ownership regardless.

Fn, FnMut and FnOnce

The three traits describe what a closure may be asked to do:

  • Fn: callable repeatedly through a shared reference. It only reads its captures.
  • FnMut: callable repeatedly through a mutable reference. It may change what it captured.
  • FnOnce: callable at most once, by value. It may consume (move out) what it captured.

Every closure is at least FnOnce. A closure that does not move anything out is also FnMut, and one that does not mutate is also Fn. So the traits nest, and a function that asks for the weakest one accepts the most closures:

src/main.rsrust
fn call_twice<F: Fn() -> i32>(f: F) -> i32 {
    f() + f()
}

fn call_mut<F: FnMut() -> i32>(mut f: F) -> i32 {
    f() + f()
}

fn call_once<F: FnOnce() -> String>(f: F) -> String {
    f()
}

fn main() {
    let base = 10;
    println!("{}", call_twice(|| base + 1));                 // reads only: Fn

    let mut n = 0;
    println!("{}", call_mut(|| { n += 5; n }));              // mutates: FnMut
    println!("n = {n}");

    let owned = String::from("moved out");
    println!("{}", call_once(move || owned));                // consumes: FnOnce
}
Text
22
15
n = 10
moved out

call_twice requires Fn and gets a closure that only reads base, so it returns 11 + 11 = 22. call_mut requires FnMut and its argument is declared mut f, because calling an FnMut needs a mutable binding; the closure adds 5 to n on each call and returns 5 then 10, for a total of 15, and afterwards n is 10. call_once takes an FnOnce and the closure returns the String it owns.

The rule for writing your own functions: ask for FnOnce if you call the closure once, FnMut if you call it several times, and Fn only if you must (for instance, when the function shares the closure across threads). Asking for more than you need rejects closures that would have worked.

The error when the trait does not fit

Pass a consuming closure where Fn is demanded:

src/main.rsrust
fn call_twice<F: Fn() -> String>(f: F) -> String {
    f() + &f()
}

fn main() {
    let owned = String::from("once");
    println!("{}", call_twice(move || owned));
}
Text
error[E0507]: cannot move out of `owned`, a captured variable in an `Fn` closure
 --> src/main.rs:7:39
  |
6 |     let owned = String::from("once");
  |         -----   -------------------- move occurs because `owned` has type `String`, which does not implement the `Copy` trait
  |         |
  |         captured outer variable
7 |     println!("{}", call_twice(move || owned));
  |                               ------- ^^^^^ `owned` is moved here
  |                               |
  |                               captured by this `Fn` closure
  |
help: `Fn` and `FnMut` closures require captured values to be able to be consumed multiple times, but `FnOnce` closures may consume them only once

The closure move || owned returns owned, moving the String out. It can be called once, because after that the string is gone, so it is only FnOnce. call_twice wants to call it twice. The compiler words it as "cannot move out of owned, a captured variable in an Fn closure", and the help text points at the bound on line 1: Fn and FnMut closures need captured values that can be consumed multiple times. Fix it either by changing what the closure does (return a clone, owned.clone()), or by loosening the bound to FnOnce and calling it once. The standard library's Option::unwrap_or_else takes an FnOnce for exactly this reason.

move and threads

A thread may outlive the function that started it, so it cannot borrow from that function's stack:

src/main.rsrust
use std::thread;

fn main() {
    let data = vec![1, 2, 3];
    let handle = thread::spawn(|| {
        println!("{data:?}");
    });
    handle.join().unwrap();
}
Text
error[E0373]: closure may outlive the current function, but it borrows `data`, which is owned by the current function
 --> src/main.rs:5:32
  |
5 |     let handle = thread::spawn(|| {
  |                                ^^ may outlive borrowed value `data`
6 |         println!("{data:?}");
  |                    ---- `data` is borrowed here
  |
note: function requires argument type to outlive `'static`
 --> src/main.rs:5:18
  |
5 |       let handle = thread::spawn(|| {

E0373: the closure may outlive the current function, but it borrows data. The compiler already prints the fix, the move keyword, which makes the closure own data:

src/main.rsrust
use std::thread;

fn main() {
    let data = vec![1, 2, 3];
    let handle = thread::spawn(move || {
        println!("{data:?}");
    });
    handle.join().unwrap();
}
Text
[1, 2, 3]

The same requirement shows up anywhere a closure is stored for later: spawned threads, async tasks, callbacks held in a struct. That is why the signature of tokio::spawn asks for 'static. If several threads need the same data, wrap it in an Arc and clone the Arc into each move closure.

Returning and storing closures

A closure has a type no one can write down, so returning one takes either impl Trait or a box:

src/main.rsrust
fn adder(by: i32) -> impl Fn(i32) -> i32 {
    move |x| x + by
}

fn pick(op: &str) -> Box<dyn Fn(i32, i32) -> i32> {
    match op {
        "add" => Box::new(|a, b| a + b),
        "mul" => Box::new(|a, b| a * b),
        _ => Box::new(|_, _| 0),
    }
}

fn main() {
    let add5 = adder(5);
    println!("{}", add5(10));
    println!("{} {}", pick("add")(3, 4), pick("mul")(3, 4));
}
Text
15
7 12

adder returns impl Fn(i32) -> i32: one concrete closure type that the caller does not name. The move is needed because by is a parameter that dies when adder returns. pick returns Box<dyn Fn(i32, i32) -> i32> because its match arms produce three different closures, and an impl Trait return must be a single type. Boxing erases the difference at the cost of a heap allocation and an indirect call. Which to choose is the static-versus-dynamic question in static and dynamic dispatch.

The same limit appears with if:

src/main.rsrust
fn main() {
    let which = true;
    let (a, b) = (1, 2);
    let f = if which { move |x: i32| x + a } else { move |x: i32| x + b };
    println!("{}", f(1));
}
Text
error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:4:53
  |
4 |     let f = if which { move |x: i32| x + a } else { move |x: i32| x + b };
  |                        -------------------          ^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure
  |                        |
  |                        the expected closure
  |                        expected because of this
  |

"No two closures, even if identical, have the same type". Each closure expression creates its own anonymous struct. The note suggests boxing, and the fixed version prints 2:

src/main.rsrust
fn main() {
    let which = true;
    let (a, b) = (1, 2);
    let f: Box<dyn Fn(i32) -> i32> = if which { Box::new(move |x| x + a) } else { Box::new(move |x| x + b) };
    println!("{}", f(1));
}
Text
2

A trap here: when the closures capture nothing, Rust converts them to plain function pointers and the if compiles. The first draft of this example used |x| x + 1 and |x| x + 2 and it compiled, which made the error vanish. Once the closures capture a variable, that conversion is not available.

What a closure costs

A closure is a struct holding its captures, and the compiler generates a distinct type for it, so a call is a direct call that can be inlined. The size is the captures:

src/main.rsrust
use std::mem::size_of_val;

fn main() {
    let nothing = |x: i32| x + 1;
    let a = 1u64;
    let one = move |x: i32| x + a as i32;
    let s = String::from("hello");
    let two = move || s.len();
    let boxed: Box<dyn Fn(i32) -> i32> = Box::new(nothing);

    println!("captures nothing : {} bytes", size_of_val(&nothing));
    println!("captures a u64   : {} bytes", size_of_val(&one));
    println!("captures a String: {} bytes", size_of_val(&two));
    println!("Box<dyn Fn>      : {} bytes (+ the heap value)", size_of_val(&boxed));
    println!("fn pointer       : {} bytes", size_of_val(&(main as fn())));
}
Text
captures nothing : 0 bytes
captures a u64   : 8 bytes
captures a String: 24 bytes
Box<dyn Fn>      : 16 bytes (+ the heap value)
fn pointer       : 8 bytes

A closure that captures nothing is zero bytes. One that captures a u64 is 8 bytes, a String 24. A Box<dyn Fn> is 16 bytes, a data pointer and a vtable pointer, plus the closure's own size on the heap. A function pointer is 8 bytes. In generic code (fn f<F: Fn()>) each closure produces its own copy of the function, the monomorphization described in generics and monomorphization: fast, at the price of code size when you pass many different closures.

Everyday use is mostly with the standard library:

src/main.rsrust
fn main() {
    let mut people = vec![("ada", 36), ("grace", 45), ("linus", 28)];
    let min_age = 30;

    people.retain(|&(_, age)| age >= min_age);         // closure captures min_age
    people.sort_by_key(|&(name, _)| std::cmp::Reverse(name.len()));
    println!("{people:?}");

    let mut log = Vec::new();
    people.iter().for_each(|(name, _)| log.push(name.to_uppercase()));
    println!("{log:?}");
}
Text
[("grace", 45), ("ada", 36)]
["GRACE", "ADA"]

retain takes an FnMut and the closure reads min_age from the surrounding scope. sort_by_key sorts by the negated name length through Reverse. The last for_each pushes into log, which is a mutable capture inside an FnMut. Iterators are where most closures live.

Habits

  • Ask for the weakest trait in your own APIs: FnOnce > FnMut > Fn.
  • Add move when the closure leaves the current function; do not add it by reflex everywhere.
  • Prefer impl Fn for returning one kind of closure and Box<dyn Fn> for storing many kinds.
  • If a closure grows past a few lines, name it as a function.
  • Capture a clone of what you need (let tx = tx.clone();) rather than fighting the borrow checker over a long-lived borrow.

When a plain function is better

If the closure captures nothing and is used in one place, a fn item or a method path (.map(str::len)) can be clearer and needs no explanation. If the callback is the heart of a public API, a trait with a named method documents itself better than an Fn bound with a three-line signature. And when the captured state grows to five or six variables, that is a struct with a method waiting to be written.

Also inGenerics
Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.