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 onei32calledx.x * 2— the body. A one-expression closure doesn’t need{ }or areturn; 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 * 2isn’t a closure. The shape is|params| body. - Using a captured variable after
move. Once you writemove ||, captured owning values (like aString) are moved into the closure; touching the original afterward givesvalue moved. Only addmovewhen you actually need the closure to own its captures. - Expecting a closure to work in a place a plain
fnis 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 mentionsexpected 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;afterx + 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
fncan’t do that. - By default closures borrow what they capture; add
moveto make them own it (needed when the closure outlives the scope, e.g. threads). - Functions accept closures via the
Fn/FnMut/FnOncetrait bounds. - Keep closures small; promote big logic to a named function.
Go deeper
- Rust Book - Closures — How closures capture state.
Next: