19.1Closures

Last updated June 13, 2026

You've written a lot of functions. Every one of them has been a named, top-level thing: fn main, fn largest, a method in an impl block. A function written that way can only see what you hand it through its parameters; it knows nothing about the code around its definition, because there is no "around", it sits at the top level on its own. A closure is a function you can write inline, right where you use it, and that can see the variables in the scope where you wrote it. That second half is the whole point, and it's where the name comes from: the closure closes over the variables it uses.

This chapter is, in a sense, where Rust starts paying you back. Closures and iterators are the tools that turn the loops you've been writing by hand into short, declarative pipelines, and almost every convenient method in the standard library takes a closure. We'll start with closures alone, then spend the rest of the chapter on the iterators that make them sing.

A first closure

Here's a closure that adds one to a number, stored in a variable and then called:

fn main() {
    let add_one = |x| x + 1;
    let result = add_one(5);
    println!("{result}");
}
6

The |x| x + 1 is the closure. The parameters go between the vertical bars (here just x), and the body comes after. We call it exactly like a function: add_one(5). If the body needs more than one expression, wrap it in braces:

let describe = |x| {
    let doubled = x * 2;
    format!("{x} doubled is {doubled}")
};
println!("{}", describe(4));
4 doubled is 8

Compare a closure to the equivalent function and the family resemblance is clear:

fn  add_one_fn(x: i32) -> i32 { x + 1 }   // a function
let add_one_cl = |x: i32| -> i32 { x + 1 };   // the same, as a closure

The closure syntax is lighter: the bars instead of parentheses, and crucially, the type annotations are optional. A top-level fn must spell out every parameter and return type, because it's a public contract that anyone might call. A closure is usually a throwaway used right here, so the compiler infers its types from how it's called, the same inference you've relied on for let since chapter 1. You can annotate a closure (the add_one_cl line above does), and occasionally you'll have to, but most closures you write will have none.

A closure's types are inferred once, then fixed

Because a closure's types are inferred from use, the first call pins them down. After that, the closure has one concrete signature like any function:

let identity = |x| x;
let s = identity(String::from("hi"));   // x is now String, forever
let n = identity(5);                     // error: expected String, found integer

The compiler reports expected struct String, found integer on the second call. A closure isn't generic; it just lets you skip writing the types it does have. If you need it to work for many types, that's a generic function (chapter 15), not a closure.

Capturing the environment

Inline syntax is a nicety. Capturing is the feature. A closure can use variables that are in scope where it's defined, not just its own parameters:

fn main() {
    let threshold = 10;
    let is_big = |x| x > threshold;

    println!("{}", is_big(15));
    println!("{}", is_big(3));
}
true
false

is_big takes one parameter, x, but it also reads threshold, a variable from main. It captured it. A plain fn can't do this; if you tried to write fn is_big(x: i32) -> bool { x > threshold }, you'd get an error that threshold isn't in scope, because a function body can only see its parameters and globals. The closure can see threshold because it was defined in the scope where threshold lives.

This is why closures matter for the rest of the chapter. When you tell a list to "keep every element greater than threshold", you hand it a closure, and the closure carries threshold along with it. The behavior and the data it needs travel together as one value.

Capturing by reference

The natural question after chapter 8: when a closure captures threshold, does it take it? By default, a closure captures by the least invasive means that still works. If it only reads a captured value, it captures a shared reference, exactly the & borrow from chapter 9:

fn main() {
    let greeting = String::from("hello");
    let print_it = || println!("{greeting}");

    print_it();
    print_it();
    println!("{greeting} is still usable here");
}
hello
hello
hello is still usable here

The closure print_it only reads greeting, so it borrows it with &. That's a shared borrow, so we can call the closure twice and still use greeting afterward, all the chapter-9 rules apply unchanged. (Note print_it has empty bars ||: it takes no parameters of its own, it works entirely off the captured greeting.)

If the closure mutates a captured value, it captures a mutable reference &mut instead, and then the closure variable itself must be mut:

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

    increment();
    increment();
    println!("{count}");
}
2

increment holds a &mut count for as long as it's alive. While that mutable borrow exists you can't also read count (the chapter-9 rule again), which is why the println! waits until after the last call. The compiler chooses &, &mut, or, as we'll see next lesson, a move, based on what the closure's body actually does. You don't ask; it figures out the lightest capture that compiles.

Capturing by move

Sometimes borrowing isn't enough: you want the closure to own what it captures. The move keyword forces every capture to be taken by value:

fn main() {
    let name = String::from("Ada");
    let greet = move || println!("Hi, {name}");

    greet();
    // name has been moved into the closure; it's gone here.
}
Hi, Ada

With move, name is moved into greet, so main can no longer use name afterward (it's been moved, chapter 8). Why would you want that? Because a borrowing closure can't outlive what it borrows. The moment a closure needs to travel somewhere the original variable won't follow, to another thread (chapter 22), or returned out of the function that made it, it has to own its captures, and move is how you say so. We'll meet the returning case in the next lesson and the thread case in chapter 22; for now, the rule is simply that move makes the closure take ownership.

Key insight

A closure bundles two things: code to run, and the captured environment it needs to run. The capture mode, &, &mut, or owned, is just the chapter-8-and-9 ownership system applied to that bundle. There's no new memory model here. A closure is data (its captures) plus behavior, and Rust borrows or moves that data by exactly the rules you already know.

Quiz time

Question #1

What can a closure do that a plain fn cannot?

Show solution

Capture variables from the scope where it's defined. A fn can only use its parameters and globals; a closure can also read or modify the surrounding local variables (it "closes over" them). It can also be written inline with inferred types, but capturing is the defining capability.

Question #2

By default, does a closure capture a variable by reference or by value? When does move change that?

Show solution

By default a closure captures by the least invasive means that compiles: a shared reference & if it only reads the value, a mutable reference &mut if it modifies it, and by value only if the body requires ownership. The move keyword forces every capture to be by value (owned), which you need when the closure must outlive the original variable, for example to send it to another thread (chapter 22) or return it from a function.

Question #3

Will this compile? If not, why?

let mut total = 0;
let add = |x| total += x;
println!("{total}");
add(5);
Show solution

No. add mutates total, so it captures &mut total and must itself be declared mut (let mut add = ...). On top of that, the println! tries to read total while add still holds a mutable borrow of it, which is the E0502 conflict from chapter 9. Two fixes are needed: make add mutable, and move the println! to after the last call to add.

Closures capture their environment, but to pass one to another function, or store it, or return it, the compiler needs to talk about a closure's type. That's the job of the Fn traits, and the next lesson (19.2) introduces them.