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

Defining Closures in Rust

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:

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

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

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

Exit mobile version