Defining Closures in Rust Programming Language: Anonymous Functions, Capturing Variables, and Fn Traits

Defining Closures in Rust

Closures were one of those Rust features I underestimated for the longest time. I treated them like lightweight, anonymous versions of regular functions and moved on. It wasn’t until I hit a compiler error about FnMut versus FnOnce that I realized closures in Rust are actually a deep feature tied directly into the ownership system. They’re not just syntactic sugar — they’re little structs the compiler generates behind the scenes, and understanding that changes how you write them.

In this article I’m going to walk through closures from the ground up: how to define them, how variable capturing actually works, what the Fn, FnMut, and FnOnce traits mean, and where closures show up in real, everyday Rust code.

What Is a Closure?

A closure is an anonymous function you can store in a variable, pass as an argument, or return from another function — and unlike a regular fn, it can capture variables from the scope it was defined in.

Here’s the simplest possible closure:

fn main() {
    let add_one = |x: i32| x + 1;
    println!("{}", add_one(5));
}

Output:

6

Compare that to a regular function doing the same thing:

fn add_one_fn(x: i32) -> i32 {
    x + 1
}

The syntax difference is small — pipes | | instead of parentheses, and often no explicit type annotations needed because Rust can infer them from how the closure is used. But the real difference is what happens with variables from the surrounding environment.

Capturing Variables From the Environment

This is the defining feature of closures. A regular function can’t reach outside its own body to grab a variable from wherever it was defined — a closure can.

fn main() {
    let discount_rate = 0.15;

    let apply_discount = |price: f64| price - (price * discount_rate);

    println!("Price after discount: {:.2}", apply_discount(200.0));
}

Output:

Price after discount: 170.00

Here, apply_discount reaches into the enclosing scope and grabs discount_rate without it being passed in as a parameter. This is what makes closures so useful for callbacks, iterators, and functional-style code.

Three Ways of Capturing: Borrow, Mutable Borrow, or Move

Rust’s closures capture variables in the least restrictive way that satisfies how the closure body uses them. Understanding this is key to understanding closure-related compiler errors.

1. Immutable Borrow

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

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

    greet();
    greet();
    println!("Still usable here: {}", name);
}

Output:

Hello, Rustacean!
Hello, Rustacean!
Still usable here: Rustacean

Since the closure only reads name, it captures it by immutable reference (&name), and main can still use name afterward.

2. Mutable Borrow

fn main() {
    let mut counter = 0;

    let mut increment = || {
        counter += 1;
        println!("Counter is now {}", counter);
    };

    increment();
    increment();
    increment();
}

Output:

Counter is now 1
Counter is now 2
Counter is now 3

Because the closure mutates counter, it captures it by mutable reference, which means increment itself must be declared mut, and you can’t use counter elsewhere while the closure is alive.

3. Move (Taking Ownership)

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

    let sum_closure = move || {
        let sum: i32 = data.iter().sum();
        println!("Sum: {}", sum);
    };

    sum_closure();
    // println!("{:?}", data); // this would fail — data was moved
}

Output:

Sum: 15

The move keyword forces the closure to take ownership of everything it captures, rather than borrowing. This is essential when you’re sending a closure to another thread, or returning a closure from a function, because the closure needs to own its data independently of the original scope.

The Fn, FnMut, and FnOnce Traits

Every closure in Rust implements one or more of three traits, and this is what the compiler uses to decide what you’re allowed to do with a closure:

  • Fn — the closure only borrows captured variables immutably; you can call it repeatedly.
  • FnMut — the closure borrows at least one variable mutably; you can call it repeatedly, but you need mutable access to the closure itself.
  • FnOnce — the closure takes ownership of at least one captured variable and consumes it; it can only be called once.

Every closure that implements Fn also implements FnMut and FnOnce, and every FnMut also implements FnOnce — the traits form a hierarchy of decreasing restriction.

Here’s a function that accepts each trait:

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

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

fn call_fn_once<F: FnOnce()>(f: F) {
    f();
}

fn main() {
    let greeting = String::from("Hi there");
    call_fn(|| println!("Fn: {}", greeting));

    let mut count = 0;
    call_fn_mut(|| {
        count += 1;
        println!("FnMut: {}", count);
    });

    let owned = String::from("consumed");
    call_fn_once(move || println!("FnOnce: {}", owned));
}

Output:

Fn: Hi there
FnMut: 1
FnMut: 2
FnOnce: consumed

Notice call_fn calls the closure twice — that’s fine because Fn closures only borrow. call_fn_mut also calls twice, but needs f declared as mut. call_fn_once only calls once, because the closure consumes owned by moving it into println!.

If you tried to call the FnOnce closure a second time, the compiler would refuse, because the captured value has already been moved out and can’t be used again.

Closures as Function Parameters and Return Values

Passing closures into functions is one of the most common uses, especially with iterator methods:

fn apply_twice<F: Fn(i32) -> i32>(f: F, value: i32) -> i32 {
    f(f(value))
}

fn main() {
    let result = apply_twice(|x| x * 2, 3);
    println!("Result: {}", result);
}

Output:

Result: 12

Returning a closure is trickier because closures have unique, compiler-generated, unnamed types. You have to return them behind a Box<dyn Fn...> or use impl Fn... when the compiler can infer a single concrete type:

fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
    move |x| x * factor
}

fn main() {
    let triple = make_multiplier(3);
    println!("{}", triple(7));
}

Output:

21

Here move is necessary because factor must be owned by the returned closure — it can’t borrow a local variable that goes out of scope when make_multiplier returns. This is a direct consequence of Rust’s lifetime rules: a returned reference (or a closure borrowing a local) can never outlive the function that created it.

Real-World Application: Closures With Iterators

Closures are everywhere in Rust’s iterator methods, and this is where they genuinely shine in day-to-day code:

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

    let even_squares: Vec<i32> = numbers
        .iter()
        .filter(|&&n| n % 2 == 0)
        .map(|&n| n * n)
        .collect();

    println!("{:?}", even_squares);

    let total: i32 = numbers.iter().sum();
    let above_average: Vec<&i32> = numbers
        .iter()
        .filter(|&&n| (n as f64) > (total as f64 / numbers.len() as f64))
        .collect();

    println!("{:?}", above_average);
}

Output:

[4, 16, 36, 64, 100]
[6, 7, 8, 9, 10]

Both .filter() and .map() take closures implementing Fn, since iterators need to call the closure once per element without consuming it.

Storing Closures in Structs

Sometimes you want a struct to hold onto a closure — for example, building a simple event handler or callback system:

struct Button {
    label: String,
    on_click: Box<dyn Fn()>,
}

impl Button {
    fn new(label: &str, on_click: impl Fn() + 'static) -> Self {
        Button {
            label: label.to_string(),
            on_click: Box::new(on_click),
        }
    }

    fn click(&self) {
        println!("Button '{}' clicked!", self.label);
        (self.on_click)();
    }
}

fn main() {
    let submit_button = Button::new("Submit", || println!("Form submitted!"));
    submit_button.click();
}

Output:

Button 'Submit' clicked!
Form submitted!

Because closures have unnamed, unique types, storing one in a struct field requires a trait object (Box<dyn Fn()>) so the struct doesn’t need to know the closure’s exact type at compile time.

Performance Considerations

Closures that only borrow their environment and don’t need dynamic dispatch are typically zero-cost — the compiler monomorphizes generic functions like apply_twice::<F> for each concrete closure type, so there’s no runtime overhead compared to writing the logic inline. Box<dyn Fn()>, on the other hand, introduces a heap allocation and a vtable-based dynamic dispatch call, which has a small but real runtime cost. Use generic impl Fn parameters when you can, and reach for Box<dyn Fn> only when you genuinely need to store heterogeneous closures or return different closure types from different branches.

Common Mistakes

  • Forgetting move when sending closures across threads. std::thread::spawn requires 'static closures, which almost always means using move.
  • Trying to call an FnOnce closure twice. If your closure consumes a captured variable, it can only run once — redesign to borrow instead if you need repeated calls.
  • Mixing up Fn bounds in generic functions. If your function needs to call the closure more than once, don’t bound it with FnOnce.
  • Expecting closures to have a nameable type. You cannot write let f: SomeClosureType = ... — use generics, impl Fn, or Box<dyn Fn> instead.

Cargo Commands

cargo new closures_demo
cd closures_demo
cargo run
cargo check

cargo check is especially handy while experimenting with closures, since it validates your types and borrow rules without producing a full binary, making iteration faster.

FAQs

Q: What’s the difference between a closure and a function pointer (fn)? A function pointer can’t capture its environment — it’s just an address to existing code. A closure can capture variables, and under the hood is a unique compiler-generated struct that may or may not also be convertible to a function pointer if it captures nothing.

Q: Do I need to specify the closure’s parameter types? Usually no — Rust infers them from context, especially in iterator chains. You only need explicit types when the compiler can’t infer them or for clarity in complex code.

Q: Why does my closure need move even though I’m not sending it to a thread? If your closure is returned from a function or stored somewhere that outlives the current scope, move is required so the closure owns its captured data instead of holding a reference to something that will be dropped.

Q: Can a closure capture by reference and by value at the same time? Yes — Rust captures each variable independently based on how it’s used inside the closure body, unless you add move, which forces ownership of everything captured.

Troubleshooting Tips

  • “closure may outlive the current function” error — add move so the closure owns its captured variables instead of borrowing local ones.
  • “expected a closure that implements the Fn trait, but this closure only implements FnMut — you’re calling a mutating closure somewhere that expects a read-only one; check if you actually need to mutate captured state.
  • “use of moved value” after passing a closure — closures that capture by move consume their environment; if you need to reuse the original variable, clone it before moving it into the closure.

Summary

Closures in Rust give you the expressive power of anonymous, capturing functions while still fitting cleanly into Rust’s ownership and borrowing model. The Fn, FnMut, and FnOnce traits aren’t arbitrary categories — they directly reflect how a closure interacts with the variables it captures, whether that’s a read-only borrow, a mutable borrow, or full ownership. Once you understand that distinction, closures stop being mysterious syntax and become one of the most natural tools in idiomatic Rust, especially once combined with iterators.

References

  • The Rust Programming Language Book, Chapter on Closures — https://doc.rust-lang.org/book/ch13-01-closures.html
  • Rust Standard Library documentation for Fn, FnMut, FnOnce — https://doc.rust-lang.org/std/ops/trait.Fn.html
  • Rust By Example, Closures — https://doc.rust-lang.org/rust-by-example/fn/closures.html
  • Cargo Book — https://doc.rust-lang.org/cargo/

Total
0
Shares

Leave a Reply

Previous Post
Data Implementation in Rust

Data Implementation in Rust Programming Language: Structs, Enums, and Custom Data Types Explained

Next Post
Using Changeable Strings in Rust

Using Changeable Strings in Rust Programming Language: String, &str, and Mutable String Operations Guide

Related Posts