Skip to content
Codeloom
Rust

Rust Closures and Fn Traits Explained

Master Rust closures, understand Fn, FnMut, and FnOnce traits, learn how closures capture variables, and use closures effectively in your code.

·7 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • How closures capture variables from their environment
  • The difference between Fn, FnMut, and FnOnce
  • How the compiler chooses which trait to implement
  • Practical patterns for closures in APIs and iterators

Prerequisites

  • Basic Rust knowledge
  • Understanding of ownership and borrowing

Closures are anonymous functions that capture variables from their surrounding scope. They are everywhere in Rust: iterators, thread spawning, callbacks, and custom APIs all rely on them. Understanding how closures work and the three Fn traits they implement is key to writing idiomatic Rust.

Closure Basics

A closure looks like a pair of pipes || for parameters, followed by the body:

fn main() {
    // Simple closure
    let add = |a: i32, b: i32| a + b;
    println!("{}", add(3, 4)); // 7

    // Closure with a block body
    let greet = |name: &str| {
        let message = format!("Hello, {name}!");
        println!("{message}");
    };
    greet("Rust");

    // Type inference: the compiler figures out types from usage
    let double = |x| x * 2;
    println!("{}", double(5)); // 10
}

Unlike regular functions, closures can capture variables from their enclosing scope:

fn main() {
    let multiplier = 3;
    let multiply = |x| x * multiplier; // captures multiplier
    println!("{}", multiply(7)); // 21
}

How Closures Capture Variables

Closures capture variables in one of three ways, and the compiler picks the least restrictive option that works:

1. By Shared Reference (&T)

When the closure only reads the captured variable:

fn main() {
    let name = String::from("Alice");

    let greet = || println!("Hello, {name}"); // captures &name

    greet();
    greet();

    // name is still usable because it was only borrowed
    println!("Name is: {name}");
}

2. By Mutable Reference (&mut T)

When the closure modifies the captured variable:

fn main() {
    let mut count = 0;

    let mut increment = || {
        count += 1; // captures &mut count
        println!("Count: {count}");
    };

    increment(); // Count: 1
    increment(); // Count: 2

    // count is usable again after the closure's last use
    println!("Final: {count}"); // Final: 2
}

3. By Value (move)

When the closure takes ownership of the captured variable:

fn main() {
    let name = String::from("Alice");

    let greet = move || println!("Hello, {name}"); // takes ownership of name

    greet();

    // This would fail: name was moved into the closure
    // println!("{name}");
}

The move keyword forces the closure to take ownership of all captured variables. This is required when the closure outlives the scope where the variable was defined, such as when spawning threads:

use std::thread;

fn main() {
    let data = vec![1, 2, 3];

    // Without move, this would fail: data might be dropped
    // before the thread finishes
    let handle = thread::spawn(move || {
        println!("Data: {:?}", data);
    });

    handle.join().unwrap();
}

The Three Fn Traits

The compiler automatically implements one or more of three traits for each closure, based on how it uses captured variables:

FnOnce

Every closure implements FnOnce. It means the closure can be called at least once. Closures that consume a captured value (move it out) can only be called once:

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

fn main() {
    let name = String::from("Alice");

    // This closure moves name out, so it can only be called once
    let make_greeting = || format!("Hello, {name}");

    let greeting = call_once(make_greeting);
    println!("{greeting}");

    // Cannot call make_greeting again: it was consumed by call_once
}

FnMut

Closures that mutate captured variables (but do not consume them) implement FnMut. They can be called multiple times:

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

fn main() {
    let mut total = 0;

    call_twice(|| {
        total += 10;
    });

    println!("Total: {total}"); // 20
}

Fn

Closures that only read captured variables (or capture nothing) implement Fn. They can be called any number of times and shared between threads:

fn apply_to_all<F: Fn(i32) -> i32>(values: &[i32], f: F) -> Vec<i32> {
    values.iter().map(|&x| f(x)).collect()
}

fn main() {
    let factor = 3;
    let results = apply_to_all(&[1, 2, 3, 4], |x| x * factor);
    println!("{:?}", results); // [3, 6, 9, 12]
}

The Trait Hierarchy

The traits form a hierarchy: Fn is a subtrait of FnMut, which is a subtrait of FnOnce.

// Fn: &self      -> can call any number of times, no mutation
// FnMut: &mut self -> can call any number of times, may mutate
// FnOnce: self    -> can call at least once, may consume

// If you need the most flexible parameter, use FnOnce
// If you need to call multiple times, use FnMut
// If you need to call multiple times without mutation, use Fn

This means a closure that implements Fn also implements FnMut and FnOnce. You can pass an Fn closure anywhere an FnOnce is expected.

Closures in Practice

With Iterators

Iterators are the most common place you will use closures:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    let result: Vec<i32> = numbers
        .iter()
        .filter(|&&x| x % 2 == 0)   // Fn: only reads x
        .map(|&x| x * x)             // Fn: only reads x
        .collect();

    println!("{:?}", result); // [4, 16, 36, 64, 100]

    // fold uses FnMut because it mutates the accumulator
    let sum = numbers.iter().fold(0, |acc, &x| acc + x);
    println!("Sum: {sum}"); // 55
}

Returning Closures

To return a closure from a function, use impl Fn or Box<dyn Fn>:

// impl Fn: static dispatch, single concrete type
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
    move |x| x + n
}

// Box<dyn Fn>: dynamic dispatch, can return different closures
fn make_operation(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(|a, _b| a),
    }
}

fn main() {
    let add_five = make_adder(5);
    println!("{}", add_five(10)); // 15

    let op = make_operation("mul");
    println!("{}", op(6, 7)); // 42
}

Storing Closures in Structs

Use generics or trait objects to store closures in structs:

// Generic: monomorphized, faster
struct Callback<F: Fn(i32)> {
    func: F,
}

impl<F: Fn(i32)> Callback<F> {
    fn call(&self, value: i32) {
        (self.func)(value);
    }
}

// Trait object: flexible, can store different closures
struct EventHandler {
    handlers: Vec<Box<dyn Fn(&str)>>,
}

impl EventHandler {
    fn new() -> Self {
        EventHandler { handlers: vec![] }
    }

    fn on_event(&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 events = EventHandler::new();

    events.on_event(|e| println!("Logger: {e}"));
    events.on_event(|e| println!("Monitor: {e}"));

    events.emit("user_login");
    // Logger: user_login
    // Monitor: user_login
}

Closures as Strategy Pattern

struct Sorter<F: Fn(&str, &str) -> std::cmp::Ordering> {
    compare: F,
}

impl<F: Fn(&str, &str) -> std::cmp::Ordering> Sorter<F> {
    fn sort(&self, items: &mut [String]) {
        items.sort_by(|a, b| (self.compare)(a, b));
    }
}

fn main() {
    let mut names = vec![
        "Charlie".to_string(),
        "alice".to_string(),
        "Bob".to_string(),
    ];

    // Case-insensitive sort
    let sorter = Sorter {
        compare: |a: &str, b: &str| a.to_lowercase().cmp(&b.to_lowercase()),
    };

    sorter.sort(&mut names);
    println!("{:?}", names); // ["alice", "Bob", "Charlie"]
}

Choosing the Right Fn Trait

When writing functions or structs that accept closures:

  • Use Fn if you need to call the closure multiple times and it should not mutate anything. This is the most restrictive but most flexible for callers.
  • Use FnMut if the closure needs to mutate its captured state. Iterator methods like fold use this.
  • Use FnOnce if you only call the closure once. This is the least restrictive and accepts all closures. Use it when you can.

A good default: accept FnOnce when you only call the closure once, and Fn when you call it multiple times.

Wrapping Up

Closures in Rust capture variables by shared reference, mutable reference, or by value. The compiler picks the least restrictive mode automatically, and the move keyword forces ownership transfer. The three Fn traits (Fn, FnMut, FnOnce) describe how the closure uses its captures. Understanding this system lets you write flexible APIs, use iterators effectively, and build callback-driven architectures that are both safe and performant.