3 min read
On this page

Closures

Closures are anonymous functions that capture variables from their surrounding scope. They are the glue that makes iterators, callbacks, and higher-order functions work in Rust. The syntax is concise, the performance is zero-cost, and the type system ensures closures interact correctly with ownership and borrowing.

Basic Syntax

fn main() {
    // Closure with explicit types
    let add = |x: i32, y: i32| -> i32 { x + y };

    // Type inference — Rust figures it out from usage
    let double = |x| x * 2;

    // Single expression — no braces needed
    let is_positive = |x: i32| x > 0;

    // Multi-line closure
    let describe = |x: i32| {
        if x > 0 {
            format!("{} is positive", x)
        } else if x < 0 {
            format!("{} is negative", x)
        } else {
            String::from("zero")
        }
    };

    println!("{}", add(3, 4));
    println!("{}", double(21));
    println!("{}", is_positive(-5));
    println!("{}", describe(42));
}
7
42
false
42 is positive

Closures look like |parameters| body. The vertical bars replace the parentheses of a function signature. Type annotations are optional when the compiler can infer them.

Capturing Variables

Closures capture variables from their enclosing scope. How they capture depends on how the variable is used:

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

    // Captures `name` by reference and `threshold` by copy
    let greet = |greeting: &str| {
        println!("{}, {}! (threshold: {})", greeting, name, threshold);
    };

    greet("Hello");
    greet("Hi");

    // name is still valid — it was borrowed, not moved
    println!("Name is still: {}", name);
}
Hello, Alice! (threshold: 10)
Hi, Alice! (threshold: 10)
Name is still: Alice

The compiler captures by the least restrictive method possible:

  1. By immutable reference (&T) — if the closure only reads the value
  2. By mutable reference (&mut T) — if the closure modifies the value
  3. By value (move) — if the closure takes ownership
fn main() {
    // Capture by immutable reference
    let data = vec![1, 2, 3];
    let print_data = || println!("{:?}", data);
    print_data();
    println!("Still here: {:?}", data);

    // Capture by mutable reference
    let mut count = 0;
    let mut increment = || {
        count += 1;
        println!("Count: {}", count);
    };
    increment();
    increment();
    // cannot use count here while increment borrows it mutably
    drop(increment); // release the borrow
    println!("Final count: {}", count);

    // Capture by value (move)
    let name = String::from("Bob");
    let consume = || {
        let local = name; // moves name into the closure
        println!("Consumed: {}", local);
    };
    consume();
    // name is no longer valid — it was moved into the closure
}
[1, 2, 3]
Still here: [1, 2, 3]
Count: 1
Count: 2
Final count: 2
Consumed: Bob

Fn, FnMut, FnOnce: The Three Closure Traits

Every closure implements one or more of these traits, determined by how it captures and uses its environment:

// Fn: borrows captured variables immutably. Can be called multiple times.
fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
    f(f(x))
}

// FnMut: borrows captured variables mutably. Can be called multiple times.
fn apply_and_count<F: FnMut(i32) -> i32>(mut f: F, values: &[i32]) -> Vec<i32> {
    values.iter().map(|&x| f(x)).collect()
}

// FnOnce: takes ownership of captured variables. Can only be called once.
fn consume<F: FnOnce() -> String>(f: F) -> String {
    f()
}

fn main() {
    // Fn: only reads the multiplier
    let multiplier = 3;
    let triple = |x| x * multiplier;
    println!("Triple 5 twice: {}", apply_twice(triple, 5));

    // FnMut: modifies the counter
    let mut calls = 0;
    let counting_double = |x: i32| {
        calls += 1;
        x * 2
    };
    let results = apply_and_count(counting_double, &[1, 2, 3]);
    println!("Results: {:?}, calls: {}", results, calls);

    // FnOnce: consumes the string
    let name = String::from("world");
    let greeting = || format!("Hello, {}!", name);
    // name is moved into the closure
    println!("{}", consume(greeting));
    // greeting cannot be called again — it was consumed by consume()
}
Triple 5 twice: 45
Results: [2, 4, 6], calls: 3
Hello, world!

The hierarchy: FnOnce is the most general (all closures implement it). FnMut refines it (can be called multiple times). Fn is the most restrictive (no mutation of captures).

move Closures

The move keyword forces a closure to take ownership of all captured variables, regardless of how they are used:

use std::thread;

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

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

    // message is no longer valid here — it was moved into the thread
    handle.join().unwrap();

    // move with Copy types: the value is copied, not moved
    let x = 42;
    let closure = move || println!("x = {}", x);
    closure();
    println!("x is still: {}", x); // x was copied, not moved
}
Hello from thread
x = 42
x is still: 42

move is essential for threads and async code where closures outlive the scope they were created in.

Closures as Function Arguments

Most iterator methods take closures. Here is how they work as function parameters:

fn filter_and_transform<F, G>(data: &[i32], predicate: F, transform: G) -> Vec<i32>
where
    F: Fn(&i32) -> bool,
    G: Fn(i32) -> i32,
{
    data.iter()
        .filter(|x| predicate(x))
        .map(|&x| transform(x))
        .collect()
}

fn apply_to_each<F: FnMut(&str)>(items: &[&str], mut action: F) {
    for item in items {
        action(item);
    }
}

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

    let result = filter_and_transform(
        &numbers,
        |&&x| x > 0,
        |x| x * x,
    );
    println!("Positive squares: {:?}", result);

    let mut log = Vec::new();
    apply_to_each(&["hello", "world", "rust"], |s| {
        log.push(s.to_uppercase());
    });
    println!("Log: {:?}", log);
}
Positive squares: [1, 9, 25]
Log: ["HELLO", "WORLD", "RUST"]

Use Fn when the closure only reads its captures. Use FnMut when it modifies them. Use FnOnce when it consumes them.

Returning Closures

Functions can return closures using impl Fn:

fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
    move |x| x + n
}

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

fn make_validator(min: i32, max: i32) -> impl Fn(i32) -> bool {
    move |x| x >= min && x <= max
}

fn main() {
    let add_five = make_adder(5);
    let double = make_multiplier(2.0);
    let is_valid_port = make_validator(1, 65535);

    println!("10 + 5 = {}", add_five(10));
    println!("3.14 * 2 = {}", double(3.14));
    println!("8080 valid? {}", is_valid_port(8080));
    println!("0 valid? {}", is_valid_port(0));
}
10 + 5 = 15
3.14 * 2 = 6.28
8080 valid? true
0 valid? false

impl Fn(i32) -> i32 means "some type that implements Fn(i32) -> i32." The actual closure type is anonymous — you cannot name it directly.

When Closures Help vs Hurt

Closures make code clearer when the logic is short and the intent is obvious:

fn main() {
    let users = vec![
        ("Alice", 30, true),
        ("Bob", 25, false),
        ("Charlie", 35, true),
        ("Diana", 28, true),
    ];

    // Clear: the closure's purpose is obvious
    let active_seniors: Vec<_> = users.iter()
        .filter(|(_, age, active)| *active && *age >= 30)
        .map(|(name, _, _)| *name)
        .collect();
    println!("Active 30+: {:?}", active_seniors);

    // Less clear: complex logic belongs in a named function
    let mut sorted = users.clone();
    sorted.sort_by(|a, b| {
        // Sort by active status (active first), then by age descending
        b.2.cmp(&a.2).then(b.1.cmp(&a.1))
    });
    println!("Sorted: {:?}", sorted);
}
Active 30+: ["Alice", "Charlie"]
Sorted: [("Charlie", 35, true), ("Alice", 30, true), ("Diana", 28, true), ("Bob", 25, false)]

If a closure exceeds 5-6 lines or contains complex logic, extract it into a named function. Named functions are easier to test, document, and understand.

Real-World Example: Retry with Configurable Strategy

use std::time::Duration;
use std::thread;

fn retry<F, T, E>(max_attempts: u32, delay: Duration, mut operation: F) -> Result<T, E>
where
    F: FnMut() -> Result<T, E>,
    E: std::fmt::Display,
{
    let mut last_err = None;
    for attempt in 1..=max_attempts {
        match operation() {
            Ok(value) => return Ok(value),
            Err(e) => {
                eprintln!("Attempt {}/{} failed: {}", attempt, max_attempts, e);
                last_err = Some(e);
                if attempt < max_attempts {
                    thread::sleep(delay);
                }
            }
        }
    }
    Err(last_err.unwrap())
}

fn main() {
    let mut call_count = 0;

    let result = retry(3, Duration::from_millis(100), || {
        call_count += 1;
        if call_count < 3 {
            Err(format!("service unavailable (attempt {})", call_count))
        } else {
            Ok("response data".to_string())
        }
    });

    match result {
        Ok(data) => println!("Success: {}", data),
        Err(e) => println!("Failed: {}", e),
    }
}
Attempt 1/3 failed: service unavailable (attempt 1)
Attempt 2/3 failed: service unavailable (attempt 2)
Success: response data

The retry function is generic over any closure that returns a Result. The caller provides both the operation and the retry configuration.

Common Pitfalls

  • Capturing more than intended. A closure that captures &self borrows the entire struct. If you only need one field, borrow the field before the closure: let field = &self.field; let closure = || use(field);.
  • Forgetting move for thread closures. Closures passed to thread::spawn must own their data. The compiler will tell you, but understand why: the thread may outlive the caller's scope.
  • Using FnOnce when FnMut would work. If a function parameter only needs FnOnce, callers cannot reuse the closure. Use the least restrictive trait that works.
  • Over-nesting closures. Deeply nested closures are hard to read. Extract intermediate steps into variables or named functions.
  • Not realizing closures have unique types. Two closures with the same signature have different types. You cannot put them in a Vec without boxing: Vec<Box<dyn Fn(i32) -> i32>>.
  • Mutating captured variables without mut on the binding. If a closure modifies a captured variable, the variable must be declared let mut and the closure binding must also be let mut.

Key Takeaways

  • Closures capture variables by reference, mutable reference, or value — automatically choosing the least restrictive option.
  • Fn reads captures, FnMut modifies them, FnOnce consumes them. Use the least restrictive trait in function signatures.
  • move forces ownership transfer into the closure. Essential for threads and async.
  • Closures are zero-cost. The compiler monomorphizes them and often inlines them entirely.
  • Use closures for short, clear transformations. Extract complex logic into named functions.
  • Every closure has a unique anonymous type. Use impl Fn for function returns or dyn Fn (boxed) for collections.