DEV Community

Divyesh Kakadiya
Divyesh Kakadiya

Posted on

Rust Closures — Why Are There Three? Fn, FnMut, and FnOnce Finally Explained

When I first saw this in Rust:

fn apply<F: Fn()>(f: F) {
    f();
}
Enter fullscreen mode Exit fullscreen mode

I thought — okay, Fn, fine. Then I saw FnMut. Then FnOnce. And I had no idea why there were three of them, when to use which, or what the difference actually was.

Every explanation I found went straight into "closures capture their environment by reference, by mutable reference, or by value."

That sentence is technically correct. It also told me nothing useful.

So let's do this differently. No jargon first. Just a real-life situation.


📖 A Real Life Example First

Imagine you hire three different people to do a job for you.

Person 1 — The Reader
You hand them your notebook. They read it as many times as they want. They never change anything. You can call ten of these people simultaneously — they all just read the same notebook.

Person 2 — The Editor
You hand them your notebook. They read it and make changes. Only one person can edit at a time. But you can ask them to edit multiple times.

Person 3 — The One-Time Guy
You hand them the notebook and they take it with them. They do their job — maybe they tear out all the pages and mail them somewhere. Once they're done, the notebook is gone. You can't call them again.


That's the whole idea behind Fn, FnMut, and FnOnce.

📖 Fn = The Reader — borrows, never changes, can be called many times

✏️ FnMut = The Editor — borrows and changes, can be called many times

📦 FnOnce = The One-Time Guy — takes ownership, can only be called once

Keep this in your head. Let's bring it into Rust now.


🤔 What Even Is a Closure?

Before the three traits, let's make sure closures themselves are clear.

A closure is just a function you define inline, that can reach out and use variables from the surrounding code.

let name = String::from("Ravi");

let greet = || {
    println!("Hello, {}!", name); // using `name` from outside
};

greet(); // Hello, Ravi!
Enter fullscreen mode Exit fullscreen mode

That || { ... } is a closure. The || is where parameters go (empty here). And notice — it's using name which lives outside the closure. That's called capturing the environment.

Now, how the closure captures that variable — read-only, read-write, or taking full ownership — is exactly what determines which of the three traits it gets.


📖 Fn — The Reader (Immutable Borrow)

let message = String::from("Server started");

let log = || {
    println!("{}", message); // just reading message
};

log(); // Server started
log(); // Server started again — works fine
log(); // Works a third time too
Enter fullscreen mode Exit fullscreen mode

This closure only reads message. It never changes it, never takes it away.

Rust sees this and thinks: "This closure is safe to call as many times as we want."

So Rust gives it the Fn trait.

The Reader analogy: The notebook stays on your desk. Anyone can read it. Nothing ever changes. You can call this closure a hundred times.

Where you'll see this in real code

fn run_twice<F: Fn()>(f: F) {
    f();
    f();
}

let city = String::from("Ahmedabad");
run_twice(|| println!("City: {}", city));
Enter fullscreen mode Exit fullscreen mode

run_twice calls f twice — only possible because f is Fn. If it were FnOnce, the second call would fail because the closure would be used up after the first.


✏️ FnMut — The Editor (Mutable Borrow)

let mut count = 0;

let mut increment = || {
    count += 1; // changing count — mutable borrow
    println!("Count: {}", count);
};

increment(); // Count: 1
increment(); // Count: 2
increment(); // Count: 3
Enter fullscreen mode Exit fullscreen mode

This closure is modifying count. It needs to change something outside itself.

Rust gives it FnMut — mutable, but still borrowing. Not owning.

The Editor analogy: The notebook stays with you, but the editor is actively scribbling in it. You can ask them to edit multiple times. But only one editor at a time.

Where you'll see this in real code

fn apply_three_times<F: FnMut()>(mut f: F) {
    f();
    f();
    f();
}

let mut total = 0;
apply_three_times(|| {
    total += 10;
});

println!("Total: {}", total); // Total: 30
Enter fullscreen mode Exit fullscreen mode

Notice mut f in the function signature — when you accept FnMut, you need to mark the parameter as mut because calling it changes internal state.


📦 FnOnce — The One-Time Guy (Takes Ownership)

let name = String::from("Divyesh");

let introduce = move || {
    println!("Hi, I am {}!", name);
    // name is now owned by this closure
};

introduce(); // Hi, I am Divyesh!
// introduce(); // ERROR — can't call again, name is gone
Enter fullscreen mode Exit fullscreen mode

This closure used move — it took full ownership of name. Once it runs, name is consumed. Nothing left for a second call.

Rust gives it FnOnce — once, and only once.

The One-Time Guy analogy: You handed the notebook over. They took it. It left your hands. There's no calling them back.

Where you'll see this in real code — thread::spawn

This is the most important real-world use of FnOnce:

use std::thread;

let message = String::from("Hello from thread");

thread::spawn(move || {
    println!("{}", message); // message moved into the thread
});
Enter fullscreen mode Exit fullscreen mode

thread::spawn requires FnOnce + Send + 'static. Why FnOnce? Because a spawned thread runs once. Ownership moves in, the thread runs, done.


🔑 The Hierarchy — The Key Insight Nobody Explains

Every Fn is also FnMut. Every FnMut is also FnOnce.

Think about it:

  • If a closure only reads (Fn), it can obviously also be called just once (FnOnce)
  • If a closure can be called repeatedly with mutation (FnMut), it can obviously be called just once (FnOnce)

So the traits form a hierarchy:

FnOnce          ← least restrictive — every closure implements this
  └── FnMut     ← closure can be called multiple times
        └── Fn  ← most restrictive — safe to call anywhere, anytime
Enter fullscreen mode Exit fullscreen mode

In practice: use the least restrictive bound that still does what you need.

// Need to call it once?
fn do_once<F: FnOnce()>(f: F) { f(); }

// Need to call it multiple times with state change?
fn do_many<F: FnMut()>(mut f: F) { f(); f(); f(); }

// Need to call it anywhere, read-only?
fn do_shared<F: Fn()>(f: F) { f(); f(); }
Enter fullscreen mode Exit fullscreen mode

🔔 Real Example — All Three Together

fn send_once<F: FnOnce()>(sender: F) {
    sender();
}

fn send_repeatedly<F: FnMut()>(mut sender: F, times: u32) {
    for _ in 0..times {
        sender();
    }
}

fn preview<F: Fn()>(sender: F) {
    println!("--- Preview ---");
    sender();
    sender();
    println!("--- End ---");
}

fn main() {
    // FnOnce — consumes the recipient string
    let recipient = String::from("ravi@example.com");
    send_once(move || {
        println!("Sending email to {}", recipient);
    });

    // FnMut — counts SMS sent
    let mut sms_count = 0;
    send_repeatedly(|| {
        sms_count += 1;
        println!("SMS #{} sent", sms_count);
    }, 3);

    // Fn — read-only preview
    let channel = String::from("Push Notification");
    preview(|| {
        println!("Channel: {}", channel);
    });
}
Enter fullscreen mode Exit fullscreen mode

Output:

Sending email to ravi@example.com
SMS #1 sent
SMS #2 sent
SMS #3 sent
--- Preview ---
Channel: Push Notification
Channel: Push Notification
--- End ---
Enter fullscreen mode Exit fullscreen mode

⚡ The move Keyword — Quick Clarification

move forces the closure to take ownership of captured variables instead of borrowing them.

let data = String::from("important");

// Without move — borrows data
let borrow_closure = || println!("{}", data);

// With move — owns data
let own_closure = move || println!("{}", data);
Enter fullscreen mode Exit fullscreen mode

move doesn't automatically make something FnOnce. It just means the closure owns its captured variables. Whether it's Fn, FnMut, or FnOnce still depends on what the closure does with those variables inside.


⚖️ Quick Reference Table

Trait Captures how Call how many times Real life
Fn Immutable borrow &T Unlimited The Reader
FnMut Mutable borrow &mut T Unlimited The Editor
FnOnce By value (ownership) Once only The One-Time Guy

🤔 When to Use Which — The Simple Rule

Use FnOnce when:

  • Spawning threads (thread::spawn)
  • The closure needs to consume or drop something
  • Something that should only happen once

Use FnMut when:

  • Building counters or accumulators
  • Iterators with state
  • Anything updating a variable across multiple calls

Use Fn when:

  • Callbacks called from multiple places
  • Anything you want to store and reuse safely
  • Default choice when mutation isn't needed

🦀 Why This Matters

Rust's whole promise: flexibility shouldn't cost you performance.

Instead of one vague "callable" type, Rust gives you three — each with a precise contract about what the closure does with its environment. The compiler enforces that contract at compile time. Zero runtime cost.

📖 Fn = just reads. Call it forever.

✏️ FnMut = reads and writes. Call it many times.

📦 FnOnce = takes ownership. Call it once, then it's done.

The notebook never lied to you. 📓


Follow me for more Rust deep dives 🦀


Missed the previous post? Check out Static Dispatch, Dynamic Dispatch & Monomorphization in Rust — closures and dispatch use the same underlying trait system, so that one pairs well with this.

Drop a ❤️ if this helped, and leave a comment below with which part finally made it click for you. 👇

Top comments (0)