Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Closures

Intermediate · Abstractions

What & why

A closure is a little function you write right where you use it, without giving it a name — and it can remember variables from the surrounding code. You’ve already seen them living inside iterator chains (.map(|n| n * 2)). This lesson slows down and explains what those |...| bars actually are.

The idea, slowly

A function with no name

Compare a normal function to a closure that does the same thing:

fn main() {
    // normal named function
    fn double_fn(x: i32) -> i32 {
        x * 2
    }

    // closure stored in a variable
    let double_cl = |x: i32| x * 2;

    println!("{}", double_fn(5)); // 10
    println!("{}", double_cl(5)); // 10
}

The closure is |x: i32| x * 2. Read it as:

  • |x: i32| — the parameter list, but with pipes | | instead of parentheses. Here it takes one i32 called x.
  • x * 2 — the body. A one-expression closure doesn’t need { } or a return; the last expression is the result. (You can use braces for multi-line bodies: |x| { let y = x + 1; y * 2 }.)

Rust can usually figure out the types, so you’ll often see them dropped: let double_cl = |x| x * 2;. The types get inferred from how you call it.

The superpower: capturing the environment

Here’s what makes a closure different from a plain function — it can use variables from the code around it, without you passing them in:

fn main() {
    let tax = 0.1;

    // this closure "captures" tax from the surrounding scope
    let with_tax = |price: f64| price + price * tax;

    println!("{}", with_tax(100.0)); // 110
    println!("{}", with_tax(50.0));  // 55
}

with_tax uses tax even though tax was never passed in as an argument. The closure captured it from the environment. A normal fn cannot do this — a top-level function only sees its own parameters. This is exactly why closures shine in iterator chains: .filter(|n| *n > threshold) can reach out and grab your local threshold.

What the compiler is thinking: “This closure mentions tax, which lives outside it. I need to keep tax available for the closure to use.” It quietly bundles the captured variable together with the code.

How a closure captures: borrow, or move

By default a closure captures by borrowing — it just peeks at the variable, like &:

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

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

    println!("still have: {}", name); // name is still usable — only borrowed
}

But sometimes you need the closure to own what it captures — especially if the closure will outlive the current scope (for example, handed to a thread). You force that with the move keyword:

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

    let greet = move || println!("Hello, {}", name); // takes ownership of name
    greet();
    // println!("{}", name); // ERROR now: name was moved into the closure
}

move tells the closure “take these captured variables with you.” After that, the original variable is gone from the outer scope — same move rules you learned in Ownership, just applied to captured values.

Passing a closure to a function

Functions can accept closures as arguments. You describe “a thing I can call” with the Fn trait family:

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

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

F: Fn(i32) -> i32 reads as “F is some callable that takes an i32 and returns an i32.” That’s a trait bound (from the Generics lesson), and it lets apply_twice accept any matching closure. The three closure traits are Fn (just reads captured values), FnMut (changes them), and FnOnce (consumes them) — for most beginner code, Fn is all you need to recognize.

Common mistakes

  • Pipes vs parentheses. Closure parameters go between | |, not ( ). Writing (x) x * 2 isn’t a closure. The shape is |params| body.
  • Using a captured variable after move. Once you write move ||, captured owning values (like a String) are moved into the closure; touching the original afterward gives value moved. Only add move when you actually need the closure to own its captures.
  • Expecting a closure to work in a place a plain fn is required. Some very low-level spots want a bare function pointer, not a capturing closure. If a closure captures nothing, it can coerce to a function pointer; if it captures, it can’t. The error mentions expected fn pointer, found closure.
  • Over-stuffing a closure. A closure with twenty lines of logic is harder to read than a named function. Keep closures short and near their use; promote big logic to a real fn.
  • Forgetting the return type/expression rule. In |x| x + 1, there’s no ; after x + 1 — adding one (|x| { x + 1; }) turns it into a closure that returns nothing (()), which usually breaks the caller.

More examples

Sort products by a custom key

sort_by_key takes a closure that picks the value to sort by — here, sorting a product list by price instead of name.

fn main() {
    let mut products = vec![("mouse", 25), ("keyboard", 60), ("mat", 10)];

    products.sort_by_key(|&(_, price)| price);

    println!("{:?}", products); // [("mat", 10), ("mouse", 25), ("keyboard", 60)]
}

A counter closure that remembers state

A closure that mutates a captured variable across calls — like a request counter or ID generator — needs FnMut, which is why it’s stored in a mut binding.

fn main() {
    let mut count = 0;
    let mut tick = || {
        count += 1;
        count
    };

    println!("{}", tick()); // 1
    println!("{}", tick()); // 2
    println!("{}", tick()); // 3
}

A function that builds a closure

Sometimes you want a family of closures — like discount calculators for different percentages. A function can return one, tailored by its arguments.

fn make_discounter(percent: f64) -> impl Fn(f64) -> f64 {
    move |price| price - price * percent / 100.0
}

fn main() {
    let ten_percent_off = make_discounter(10.0);
    println!("{}", ten_percent_off(200.0)); // 180
    println!("{}", ten_percent_off(50.0));  // 45
}

Pass a closure as a callback

Handing a closure into a function as “what to do with each item” is a common pattern for things like processing orders one at a time.

fn process_orders(orders: &[&str], on_each: impl Fn(&str)) {
    for order in orders {
        on_each(order);
    }
}

fn main() {
    let orders = ["order-1", "order-2", "order-3"];
    process_orders(&orders, |o| println!("shipping {o}"));
}

Filter with a captured threshold

A closure that reaches out and grabs a local variable — like a reorder threshold — is what makes .filter() so handy for one-off business rules.

fn main() {
    let inventory = vec![5, 12, 3, 20, 8];
    let low_stock_limit = 10;

    let low_stock: Vec<&i32> = inventory.iter().filter(|&&qty| qty < low_stock_limit).collect();

    println!("reorder these: {:?}", low_stock); // [5, 3, 8]
}

Your turn

This should build a closure that adds a captured bonus to any score, then apply it. It doesn’t compile — the closure syntax is wrong.

fn main() {
    let bonus = 5;

    let add_bonus = (score) score + bonus;

    println!("{}", add_bonus(10));
    println!("{}", add_bonus(20));
}
Show solution

Closure parameters go inside pipes | |, not parentheses:

fn main() {
    let bonus = 5;

    let add_bonus = |score| score + bonus; // pipes, and it captures bonus

    println!("{}", add_bonus(10)); // 15
    println!("{}", add_bonus(20)); // 25
}

The closure captures bonus from the surrounding scope, so you never pass it in explicitly.

Quick check

Remember this

  • A closure is an unnamed function written inline: |params| body.
  • Its superpower is capturing variables from the surrounding scope — a plain fn can’t do that.
  • By default closures borrow what they capture; add move to make them own it (needed when the closure outlives the scope, e.g. threads).
  • Functions accept closures via the Fn / FnMut / FnOnce trait bounds.
  • Keep closures small; promote big logic to a named function.

Go deeper

Next: