Skip to editor content
learningrust.orglesson 9 of 26

Closures

Closures are anonymous functions that can capture variables from their surrounding scope. If you have used lambdas in Python or arrow functions in JavaScript, closures in Rust fill the same role — but with the added power of Rust's ownership system. The compiler tracks exactly how each captured variable is used, giving you zero-cost abstractions without sacrificing safety.

Closures show up everywhere in idiomatic Rust. They are the glue behind iterator chains like .map() and .filter(), callback-driven APIs, and async runtimes. Understanding closures unlocks the most expressive parts of the language.

Basic Closure Syntax

A closure uses pipes | instead of parentheses for its parameters:

fn main() {
    // A regular function
    fn add_one_fn(x: i32) -> i32 {
        x + 1
    }

    // The same logic as a closure
    let add_one = |x: i32| x + 1;

    // Multi-line closures use braces
    let add_and_print = |x: i32, y: i32| -> i32 {
        let sum = x + y;
        println!("{x} + {y} = {sum}");
        sum
    };

    println!("fn:      {}", add_one_fn(5));
    println!("closure: {}", add_one(5));
    add_and_print(3, 4);
}

Unlike functions, closures can usually infer their parameter and return types from context. You only need type annotations when the compiler cannot figure it out on its own.

Capturing Variables

The real power of closures is that they can reach into the surrounding scope and use variables defined outside their body.

Capture by Reference (Borrowing)

By default, closures borrow captured variables with the least permission needed:

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

    // This closure borrows `greeting` by shared reference
    let say_hello = || println!("{greeting}");

    say_hello();
    say_hello();

    // `greeting` is still usable here because it was only borrowed
    println!("Original: {greeting}");
}

Capture by Mutable Reference

If a closure modifies a captured variable, it borrows mutably:

fn main() {
    let mut count = 0;

    // Must declare the closure `mut` because it mutates captured state
    let mut increment = || {
        count += 1;
        println!("count = {count}");
    };

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

    // `count` is accessible again after the closure is no longer used
    println!("final count = {count}");
}

Capture by Value with move

The move keyword forces a closure to take ownership of every variable it captures. This is essential when the closure needs to outlive the scope where it was created, such as when passing it to another thread:

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

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

    greet();

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

The Three Fn Traits

Every closure in Rust implements one or more of these traits. The compiler picks the most permissive trait the closure qualifies for:

TraitWhat it meansCan call multiple times?
FnBorrows captured values immutablyYes
FnMutMay mutate captured valuesYes
FnOnceConsumes captured values (takes ownership)Only once

The hierarchy is: Fn is a subtrait of FnMut, which is a subtrait of FnOnce. A closure that implements Fn also implements FnMut and FnOnce.

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

    // Fn — only reads `name`
    let greet = || println!("Hi, {name}!");
    call_twice(&greet);

    // FnMut — modifies `total`
    let mut total = 0;
    let mut adder = |x: i32| total += x;
    adder(5);
    adder(10);
    println!("total = {total}");

    // FnOnce — moves `name` out, so it can only run once
    let consume = move || {
        let _owned = name;  // takes ownership inside
        println!("Consumed!");
    };
    consume();
    // consume(); // Would not compile — already consumed
}

fn call_twice(f: &dyn Fn()) {
    f();
    f();
}

Closures as Function Parameters

Use impl Fn traits to accept closures as arguments. This is how standard library functions like map and filter work:

fn apply_to_5(f: impl Fn(i32) -> i32) -> i32 {
    f(5)
}

fn apply_twice(mut f: impl FnMut(i32) -> i32, value: i32) -> i32 {
    let first = f(value);
    f(first)
}

fn main() {
    let double = |x| x * 2;
    let square = |x| x * x;

    println!("double(5) = {}", apply_to_5(double));
    println!("square(5) = {}", apply_to_5(square));
    println!("double twice: {}", apply_twice(|x| x * 2, 3));
}

Returning Closures

Functions can return closures using impl Fn as the return type:

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

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

fn main() {
    let add_10 = make_adder(10);
    let triple = make_multiplier(3);

    println!("add_10(5) = {}", add_10(5));
    println!("triple(7) = {}", triple(7));
}

Note the move keyword: the returned closure must own its captured data because the function's local variables will be dropped when it returns.

Practical Examples

Closures with Iterators

Closures and iterators are a natural pair. Most iterator adapters accept closures:

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

    // Filter even numbers and double them
    let result: Vec<i32> = numbers
        .iter()
        .filter(|&&x| x % 2 == 0)
        .map(|&x| x * 2)
        .collect();

    println!("Even doubled: {result:?}");

    // Find the first number greater than 5
    let found = numbers.iter().find(|&&x| x > 5);
    println!("First > 5: {found:?}");

    // Sum all numbers using fold
    let sum = numbers.iter().fold(0, |acc, &x| acc + x);
    println!("Sum: {sum}");
}

Event Callbacks

Closures make clean callback APIs:

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!("[{}] clicked!", self.label);
        (self.on_click)();
    }
}

fn main() {
    let save_btn = Button::new("Save", || {
        println!("  -> Saving document...");
    });

    let counter = std::cell::Cell::new(0);
    let count_btn = Button::new("Count", move || {
        counter.set(counter.get() + 1);
        println!("  -> Clicked {} times", counter.get());
    });

    save_btn.click();
    count_btn.click();
    count_btn.click();
}

Interactive Playground

Experiment with closures below. Try modifying the captured variables, changing move semantics, or chaining more iterator methods:

fn main() {
    // Try these experiments:
    // 1. Change the threshold and see how the filter changes
    // 2. Add a .take(3) before .collect() to limit results
    // 3. Create your own closure that captures a variable

    let threshold = 3;
    let data = vec![1, 5, 2, 8, 3, 9, 4, 7];

    let above: Vec<&i32> = data
        .iter()
        .filter(|&&x| x > threshold)
        .collect();

    println!("Above {threshold}: {above:?}");

    // Make a reusable transformer
    let offset = 100;
    let transform = |x: i32| x * 2 + offset;

    for val in &data {
        println!("{val} -> {}", transform(*val));
    }
}

Practice Exercises

  1. Square closure: Write a closure that takes an i32 and returns its square. Use it with .map() on a vector of numbers.

  2. Custom filter: Create a function filter_above(data: &[i32], min: i32) -> Vec<i32> that uses a closure internally to filter values above min.

  3. Call counter: Write a function that returns a closure implementing FnMut. Each call should return how many times the closure has been invoked so far. (Hint: use move and a mutable variable.)

  4. Compose: Write a function compose that takes two closures f and g and returns a new closure that applies f(g(x)). Test it by composing "double" and "add 1".

Key Takeaways

  • Closures are anonymous functions defined with |params| body syntax
  • They automatically capture variables from their surrounding scope
  • The compiler chooses the least restrictive capture mode: by reference, by mutable reference, or by value
  • The move keyword forces ownership transfer into the closure
  • Three traits define how closures behave: Fn (immutable borrow), FnMut (mutable borrow), and FnOnce (consumes captured values)
  • Use impl Fn(...) to accept or return closures in function signatures

Next Steps

Closures give you flexible, inline functions that capture their environment — and they show up constantly in the combinator methods you're about to meet. In the next lesson, we'll tackle Option and Result, Rust's two essential enums for handling absence and errors. You'll see how methods like map and and_then take closures to transform values safely, without ever reaching for null.

Next lesson

Option and Result

Master Rust Option and Result types for handling absence and errors with combinators, the ? operator, and error propagation

25 min