RustClosures

Closures in Rust

A closure is an anonymous function that can capture variables from the scope in which it is defined. Unlike regular functions, closures remember the environment around them — they "close over" surrounding variables, which is where the name comes from.

Closures are used everywhere in Rust: with iterators, threads, error handling, and callbacks. Understanding them unlocks a large and expressive part of the language.

Basic Closure Syntax

Closures use pipes | to delimit parameters instead of parentheses, and the body can be a single expression or a block.

RUST
fn main() {
    // Simplest closure — no parameters, no return value
    let greet = || println!("Hello from a closure!");
    greet();

    // One parameter, inferred types
    let double = |x| x * 2;
    println!("double 5 = {}", double(5));

    // Explicit types and block body
    let add = |x: i32, y: i32| -> i32 {
        let sum = x + y;
        sum
    };
    println!("3 + 4 = {}", add(3, 4));
}
Hello from a closure!
double 5 = 10
3 + 4 = 7
Note
Unlike regular functions, closures usually do not need type annotations — the compiler infers both parameter types and the return type from how the closure is used. Once inferred, the types are locked in for that closure instance.
Closures Capture Their Environment

The defining trait of a closure is that it can use variables from the surrounding scope. A regular function cannot do this — it can only work with its own parameters and items in scope at the module level.

RUST
fn main() {
    let base = 10;

    // Closure captures 'base' from the enclosing scope
    let add_base = |x| x + base;

    println!("{}", add_base(5));    // 15
    println!("{}", add_base(20));   // 30

    // 'base' is still usable here because it was only borrowed
    println!("base is still: {}", base);
}
15
30
base is still: 10
How Closures Capture: Three Modes

Rust captures variables in the most permissive way the closure's body allows. The three capture modes, from least to most ownership transfer, are:

  1. Immutable borrow (&T) — the closure reads the variable but does not modify it. The original variable remains accessible to the rest of the function.

  2. Mutable borrow (&mut T) — the closure modifies the variable. No other access to the variable is allowed while the closure exists.

  3. Move / ownership (T) — the closure takes ownership of the variable. The original binding is no longer accessible after the closure is created.

RUST
fn main() {
    // Immutable borrow — closure only reads 'greeting'
    let greeting = String::from("Hello");
    let print_greeting = || println!("{}", greeting);
    print_greeting();
    println!("still have: {}", greeting); // OK

    // Mutable borrow — closure modifies 'count'
    let mut count = 0;
    let mut increment = || {
        count += 1;
        println!("count: {}", count);
    };
    increment();
    increment();
    // println!("{}", count); // would error — mutably borrowed by closure
    drop(increment);          // closure dropped, borrow ends
    println!("final count: {}", count); // OK now

    // Move — closure takes ownership of 'data'
    let data = vec![1, 2, 3];
    let owns_data = move || println!("data: {:?}", data);
    owns_data();
    // println!("{:?}", data); // ERROR — data was moved into closure
}
Hello
still have: Hello
count: 1
count: 2
final count: 2
data: [1, 2, 3]
The move Keyword

Adding move before the closure's pipes forces it to take ownership of every captured variable, even those it only reads. This is essential when the closure needs to outlive the current scope — most commonly when passing a closure to a new thread.

RUST
use std::thread;

fn main() {
    let message = String::from("Hello from thread!");

    // Without 'move' this would not compile — the thread might outlive 'message'
    let handle = thread::spawn(move || {
        println!("{}", message);
    });

    handle.join().unwrap();
    // 'message' was moved — cannot use it here
}
Hello from thread!
Tip
Use move whenever a closure is sent to another thread or stored in a struct that outlives the current function. If you only need to share data across threads, wrap it in Arc before moving.
The Three Fn Traits

Every closure in Rust automatically implements one or more of three traits that describe how it can be called. Understanding these traits is key to writing functions that accept closures as arguments.

Trait

Can be called

May consume captured values

May mutate captures

FnOnce

Once only

Yes — may move out of captured values

Yes

FnMut

Multiple times

No

Yes — mutates captured values

Fn

Multiple times

No

No — only immutable access

Every closure implements FnOnce. If it does not consume any captured value it also implements FnMut. If it does not mutate any capture it also implements Fn. This means Fn is the most restrictive and FnOnce the most permissive.

RUST
fn call_once(f: impl FnOnce() -> String) -> String {
    f() // can only call f once
}

fn call_many_times(mut f: impl FnMut() -> i32) {
    println!("{}", f());
    println!("{}", f());
    println!("{}", f());
}

fn call_without_side_effects(f: impl Fn(i32) -> i32) -> i32 {
    f(1) + f(2) + f(3)
}

fn main() {
    // FnOnce — consumes 'name' by moving it into the return value
    let name = String::from("Alice");
    let greeting = call_once(|| format!("Hello, {}!", name));
    println!("{}", greeting);

    // FnMut — mutates captured counter
    let mut n = 0;
    call_many_times(|| { n += 1; n });
    println!("n is now: {}", n);

    // Fn — only reads 'factor'
    let factor = 3;
    let total = call_without_side_effects(|x| x * factor);
    println!("total: {}", total);
}
Hello, Alice!
1
2
3
n is now: 3
total: 18
Closures as Function Arguments

Accepting a closure as a parameter is done with either impl Trait (for a concrete, statically-dispatched closure) or a generic bound. Choose the weakest trait that satisfies your needs — if you only call the closure once, use FnOnce; if you call it many times without mutation, use Fn.

RUST
// impl Fn syntax — clean and idiomatic
fn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 {
    f(f(x))
}

// Generic syntax — equivalent, useful when the bound is shared across parameters
fn apply_twice_generic<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
    f(f(x))
}

fn main() {
    let result = apply_twice(|x| x + 3, 10);
    println!("10 + 3 + 3 = {}", result);  // 16

    let result2 = apply_twice_generic(|x| x * 2, 5);
    println!("5 * 2 * 2 = {}", result2);  // 20
}
10 + 3 + 3 = 16
5 * 2 * 2 = 20
Closures as Return Values

Returning a closure from a function is slightly more involved because closures have anonymous, compiler-generated types. You have two options:

  • impl Fn(...) — concrete return type known at compile time (preferred)
  • Box&lt;dyn Fn(...)&gt; — trait object, needed when the exact closure type is not known at compile time (e.g. choosing between two different closures at runtime)

RUST
// impl Fn — zero overhead, type known at compile time
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
    move |x| x + n
}

// Box<dyn Fn> — needed when the return type varies at runtime
fn make_multiplier(double: bool) -> Box<dyn Fn(i32) -> i32> {
    if double {
        Box::new(|x| x * 2)
    } else {
        Box::new(|x| x * 3)
    }
}

fn main() {
    let add5 = make_adder(5);
    let add10 = make_adder(10);
    println!("add5(3) = {}", add5(3));   // 8
    println!("add10(3) = {}", add10(3)); // 13

    let f = make_multiplier(true);
    println!("double 7 = {}", f(7));     // 14

    let g = make_multiplier(false);
    println!("triple 7 = {}", g(7));     // 21
}
add5(3) = 8
add10(3) = 13
double 7 = 14
triple 7 = 21
Note
impl Fn in return position requires that all branches return the same concrete closure type. If your function can return different closures depending on runtime conditions, use Box<dyn Fn>.
Closures with Iterators

Closures shine when combined with Rust's iterator adapters. Methods like .map(), .filter(), .for_each(), and .sort_by() all take closures and together form a powerful, zero-overhead functional pipeline.

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

    // filter + map + collect
    let even_squares: Vec<i32> = numbers.iter()
        .filter(|&&x| x % 2 == 0)
        .map(|&x| x * x)
        .collect();
    println!("even squares: {:?}", even_squares);

    // for_each
    numbers.iter()
        .filter(|&&x| x > 7)
        .for_each(|x| print!("{} ", x));
    println!();

    // sort_by — sort strings by length
    let mut words = vec!["banana", "apple", "fig", "cherry", "date"];
    words.sort_by(|a, b| a.len().cmp(&b.len()));
    println!("sorted by length: {:?}", words);

    // fold — sum with a starting value
    let sum: i32 = numbers.iter().fold(0, |acc, &x| acc + x);
    println!("sum: {}", sum);
}
even squares: [4, 16, 36, 64, 100]
8 9 10
sorted by length: ["fig", "date", "apple", "banana", "cherry"]
sum: 55
Tip
Iterator chains are lazy — no work is done until a consuming adapter like \`.collect()\`, \`.sum()\`, or \`.for_each()\` is called. This means you can chain as many adapters as you like with no intermediate allocations.
Capturing and the Borrow Checker

Because closures borrow or move variables, they interact with the borrow checker. The most common pitfall is trying to use a captured variable after it has been moved, or holding a mutable closure and a shared reference simultaneously.

RUST
fn main() {
    let mut items: Vec<i32> = vec![3, 1, 4, 1, 5];

    // This works — sort_by takes &self, items is not moved
    items.sort_by(|a, b| a.cmp(b));
    println!("sorted: {:?}", items);

    // Mutable closure borrows 'items' — cannot read 'items' during borrow
    let mut push_item = || items.push(99);
    push_item();
    // println!("{:?}", items); // would fail — mutable borrow still active
    drop(push_item);            // end the borrow
    println!("after push: {:?}", items);
}
sorted: [1, 1, 3, 4, 5]
after push: [1, 1, 3, 4, 5, 99]
Memoization with Closures

A practical use of closures is building a simple memoization wrapper — a structure that caches the result of an expensive computation and re-uses it on subsequent calls.

RUST
struct Memoize<T, F>
where
    F: Fn(u32) -> T,
    T: Clone,
{
    func:  F,
    cache: Option<(u32, T)>,
}

impl<T, F> Memoize<T, F>
where
    F: Fn(u32) -> T,
    T: Clone,
{
    fn new(func: F) -> Self {
        Memoize { func, cache: None }
    }

    fn call(&mut self, arg: u32) -> T {
        match &self.cache {
            Some((cached_arg, cached_val)) if *cached_arg == arg => {
                println!("(cache hit for {})", arg);
                cached_val.clone()
            }
            _ => {
                println!("(computing for {})", arg);
                let result = (self.func)(arg);
                self.cache = Some((arg, result.clone()));
                result
            }
        }
    }
}

fn expensive(n: u32) -> u32 {
    // Simulate expensive work
    n * n + 1
}

fn main() {
    let mut memo = Memoize::new(expensive);
    println!("result: {}", memo.call(5));
    println!("result: {}", memo.call(5));  // cache hit
    println!("result: {}", memo.call(7));  // new computation
}
(computing for 5)
result: 26
(cache hit for 5)
result: 26
(computing for 7)
result: 50
Returning Different Closures at Runtime

Sometimes you need to choose between closures dynamically. Because different closures have different sizes (each closure is its own compiler-generated type), you must heap-allocate them with Box to return them through a uniform interface.

RUST
fn make_operation(op: &str) -> Box<dyn Fn(f64, f64) -> f64> {
    match op {
        "add" => Box::new(|a, b| a + b),
        "sub" => Box::new(|a, b| a - b),
        "mul" => Box::new(|a, b| a * b),
        "div" => Box::new(|a, b| if b != 0.0 { a / b } else { f64::NAN }),
        _     => Box::new(|_, _| 0.0),
    }
}

fn main() {
    let ops = ["add", "sub", "mul", "div"];
    for op in &ops {
        let f = make_operation(op);
        println!("{}: {}", op, f(10.0, 3.0));
    }
}
add: 13
sub: 7
mul: 30
div: 3.3333333333333335
Closures vs Functions: When to Use Which

Feature

Regular Function

Closure

Syntax

fn name(params) -> R { }

|params| expr or |params| { }

Can capture environment

No

Yes

Type annotations

Required

Usually optional (inferred)

Implements Fn traits

Yes (all three)

Yes (one or more)

Can be stored in a variable

As function pointer fn()

Directly as a value

Overhead

None

None for Fn/FnMut; heap alloc for Box<dyn Fn>

Use when

Reusable, named logic

Short, inline, context-dependent logic

Common Patterns at a Glance

RUST
fn main() {
    // Short inline transformation
    let numbers = vec![1, 2, 3];
    let doubled: Vec<_> = numbers.iter().map(|x| x * 2).collect();

    // Capture by reference for read-only work
    let threshold = 5;
    let big: Vec<_> = numbers.iter().filter(|&&x| x > threshold).collect();

    // move for thread safety
    let msg = String::from("hello");
    std::thread::spawn(move || println!("{}", msg)).join().unwrap();

    // Return a configured closure
    let add_100 = |x: i32| x + 100;
    println!("{}", add_100(42));

    // FnOnce — consume and transform
    let name = String::from("Rust");
    let shout = || format!("{}!", name.to_uppercase());
    println!("{}", shout());

    println!("doubled: {:?}", doubled);
    println!("big (>5): {:?}", big);
}
hello
142
RUST!
doubled: [2, 4, 6]
big (>5): []
Success
Closures are one of Rust's most ergonomic features. They let you pass behaviour as data, build iterator pipelines, customise algorithms, and safely share work across threads — all with zero runtime overhead when the types are known at compile time.