TL;DR
Rust closures tutorial — learn how closures capture variables, understand Fn, FnMut, and FnOnce traits, and use closures with iterators
Key concepts
- Rust closures
- Fn FnMut FnOnce
- Rust lambda
- closure tutorial
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, so you get that expressiveness without sacrificing safety — and with optimizations on, the closure inlines away to the same machine code as a direct call.
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.
What You Already Know
By the end of this lesson you will write a three-function toolkit: one that accepts a read-only closure, one that accepts a closure updating state it captured, and one that returns a closure built inside it. The interesting part is not the syntax — it is that you never declare which of the three Fn traits a closure implements. The compiler works it out from what the body does, and then the bound you wrote on the parameter decides whether that closure is allowed through.
You arrive here able to say whether a function takes a value or merely borrows it, and to read the error when a borrow outlives what it points at (Borrowing in Depth). That is exactly the machinery closures run on: a closure that only reads a captured variable holds a shared borrow of it, one that writes holds a mutable borrow, and move opts out of borrowing altogether by taking ownership. Every rule in this lesson is a borrowing rule wearing different clothes — which is why closures sit in the ownership module rather than beside functions.
And it is not only preparation. The taskwork CLI you build at the end of the track filters its task list through a closure: select calls .filter(|t| match wanted { Some(p) => t.priority == p, None => true }), a closure that captures wanted by reference and reads it once per task. That is the read-only Fn case this lesson opens with, doing the capstone's actual selecting — so the first of your three toolkit functions is the one the final program leans on.
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. Read the comments below for why the compiler chose this capture and not a stronger one — that choice is the entire subject of this section:
fn main() {
let greeting = String::from("Hello");
// The body only READS greeting, so a shared borrow is enough and that is
// what the compiler takes. Note what it did NOT do: it did not move the
// String in, even though that would also have worked, because taking more
// permission than the body needs would break the caller for no reason.
let say_hello = || println!("{greeting}");
// Two calls are possible because a shared borrow is not consumed by use.
say_hello();
say_hello();
// And this line is the proof that only a borrow was taken: if the closure
// had captured by value, greeting would be moved and this would be E0382.
println!("Original: {greeting}");
}
Capture by Mutable Reference
If a closure modifies a captured variable, it borrows mutably. One word changes in the body, and three things change as a consequence — work out what they are before reading the comments:
fn main() {
let mut count = 0;
// The body WRITES to count, so a shared borrow will not do and the
// compiler takes a mutable one instead. Two knock-on effects: count
// itself had to be declared mut, and the CLOSURE has to be mut too,
// because calling a closure that holds a mutable borrow requires
// mutably borrowing the closure.
let mut increment = || {
count += 1;
println!("count = {count}");
};
increment();
increment();
increment();
// Readable again only because the closure is finished with — the
// mutable borrow ends at the closure's LAST USE, exactly as in
// Borrowing in Depth. Move this line above the calls and it fails.
println!("final count = {count}");
}
A capture is a live borrow
That last comment is worth making concrete, because it is the closure rule most likely to surprise you. A closure holding a mutable capture is holding a mutable borrow for as long as the closure is live — and "live" means until the closure's own last use, not until the end of its scope. So an ordinary read of the captured variable, sitting between the closure's definition and its final call, is a second borrow of something already mutably borrowed.
Predict
The closure on line A captures tally mutably, line B calls it, and line C reads tally directly. As written this compiles — say what it prints. Then the harder half: swap lines B and C so the direct read comes first, and it no longer compiles. Say why, and which of the three lines the compiler would put its caret on.
fn main() {
let mut tally = vec![1, 2, 3];
let mut record = || tally.push(4); // line A: closure MUTABLY captures tally
record(); // line B: LAST use of the closure
println!("{:?}", tally); // line C: read tally directly
// Swap lines B and C and this program stops compiling.
}As written it prints [1, 2, 3, 4]. Swap the last two lines and you get error[E0502]: cannot borrow tally as immutable because it is also borrowed as mutable. The fix is never to avoid capturing — it is to notice how long the capture lives, and to end it before the access that conflicts with it.
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}");
}
That commented-out line is not a new rule; it is a rule you already have. Answer this from memory before reading on.
Recall
Without scrolling up: in Ownership & Borrowing you learned that a value is MOVED when it is handed to a new owner, and the original name is unusable afterwards. A move closure captures name by value. Which statement follows from those two facts alone?
To say it plainly: a move closure owns what it captured, exactly as a struct with a String field would. Defining it transfers ownership out of name, so reading name afterwards is the same use-after-move error from Ownership & Borrowing — and the captured String is dropped when the closure is dropped, not when the enclosing function returns. That last point is the whole reason a closure you intend to return, or hand to another thread, must capture by move: the function's locals die at the return, so a borrowing closure would be left pointing at nothing.
The Three Fn Traits
Every closure in Rust implements one or more of these traits. The compiler works out which ones a closure qualifies for from what its body actually does with its captures:
| Trait | What it means | Can call multiple times? |
|---|---|---|
Fn | Borrows captured values immutably | Yes |
FnMut | May mutate captured values | Yes |
FnOnce | Consumes 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();
}
Here is the part that catches people out: you never choose which trait a closure implements — the compiler reads the body and decides. move is not that choice. move only settles how variables get into the closure; what the body then does with them is what picks Fn, FnMut or FnOnce. The two closures below are designed to make that split visible: one has move and only reads, the other has no move and consumes. Work out what this prints, and which of them could survive line C being uncommented.
Predict
announce is a move closure whose body only reads. consume has no move at all, but its body hands the captured String away. call_twice demands impl Fn. What does this print, and what would uncommenting line C do?
fn call_twice(f: impl Fn()) {
f();
f();
}
fn main() {
let tag = String::from("ping");
let ticket = String::from("T-9");
// Closure ONE: move, but the body only READS tag.
let announce = move || println!("{} (len {})", tag, tag.len());
// Closure TWO: no move, but the body CONSUMES ticket by handing it away.
let consume = || {
let owned: String = ticket;
println!("consumed {}", owned);
};
call_twice(announce); // line A
consume(); // line B
// consume(); // line C: commented out
}It prints ping (len 4) twice, then consumed T-9. announce carries move, so it owns its tag, but its body only reads — so the compiler infers Fn, and call_twice may call it as often as it likes. consume has no move keyword, yet let owned: String = ticket; moves the String out of the closure's environment, which can only happen once — so the compiler infers FnOnce, and uncommenting line C fails with error[E0382]: use of moved value: consume and the note "closure cannot be invoked more than once because it moves the variable ticket out of its environment". The rule to keep: move decides how captures get in; the body decides which trait comes out.
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.
A factory like this is also the standard way to build a closure with memory — swap impl Fn for impl FnMut and the returned closure can update its captured state between calls. The catch is that the state belongs to the closure value, not to the factory, so where you call the factory decides what gets remembered. The next program gets that wrong. It compiles without a single warning and prints a perfectly plausible answer, which is what makes it dangerous. Commit a hypothesis before changing anything:
Debug
make_counter hands back a closure that remembers how many times it has been called, and this program uses it to stamp each event with the next sequence number. It compiles cleanly and prints numbers that look like numbers, but they are the wrong ones. Say why before you change anything, then fix it.
fn make_counter() -> impl FnMut() -> u32 {
let mut count = 0;
move || {
count += 1;
count
}
}
fn main() {
// Stamp each event with the next sequence number: 1, 2, 3, 4.
let events = ["login", "click", "click", "logout"];
let mut seen = Vec::new();
for event in &events {
let mut tally = make_counter();
seen.push(format!("{}:{}", tally(), event));
}
println!("{:?}", seen);
assert_eq!(
seen,
vec!["1:login", "2:click", "3:click", "4:logout"],
"each event should get the next sequence number, got {:?}",
seen
);
println!("All checks passed.");
}Expected output: ["1:login", "2:click", "3:click", "4:logout"]
All checks passed.
The state belongs to the closure, not to the factory, and each call to make_counter produces a fresh closure with its own count starting at zero. Because make_counter() sits inside the loop body, every iteration built a new counter, called it once, and dropped it — so every event was stamped 1 and the program printed ["1:login", "1:click", "1:click", "1:logout"]. Moving let mut tally = make_counter(); above the loop makes one closure survive all four iterations, and the stamps run 1, 2, 3, 4. The captured state lives exactly as long as the closure value that owns it, which is the move rule again with a counter attached.
Practical Examples
Closures with Iterators
Closures and iterators are a natural pair. Most iterator adapters accept closures — and the example below uses two things you have not been taught yet, so here is just enough to read it.
A Vec<T> is a growable list of values of one type, written vec![1, 2, 3]; it gets a lesson of its own later. Calling .iter() on one gives an iterator, a value that hands out each element in turn, and iterators have adapter methods that each take a closure and produce a new iterator: .filter(f) keeps the elements for which f returns true, .map(f) replaces each element with f of it, and .collect() gathers whatever is left back into a collection. Chained together they read left to right as a pipeline. Do not worry about the details of these methods here — the iterators lesson covers them properly. What matters right now is the shape: each adapter's argument is a closure, and each closure is the piece of behaviour you are handing to code someone else wrote. That is what closures are for.
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 — and the example below reaches for two more things you have not been taught, in the same spirit as the iterator note above.
A closure has no type you can write down: every one is its own anonymous type invented by the compiler, so a struct field cannot simply be declared as "a closure". Box<dyn Fn()> is the way around that. dyn Fn() means "some value, whatever its type, that can be called with no arguments and returns nothing", and Box puts it on the heap so the struct field can be one fixed size regardless of which closure was stored. The pair is why the field can hold a different closure for every button. On the constructor side, impl Fn() + 'static is the mirror image: it accepts any concrete closure type satisfying Fn(), and 'static says the closure must not borrow anything that could die before the button does — which is what forces the move at the call site. Smart pointers get their own lesson later and so do explicit lifetimes; read Box<dyn Fn()> here as "a callable stored by the struct" and 'static as "it owns what it captured", and the example reads fine.
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...");
});
// `move` MOVES `document` into the closure — String is not Copy, so
// ownership transfers and the local is gone afterwards. That is what
// lets the Button outlive the scope it was built in, and it is the
// whole reason a callback needs `move` and a direct call does not.
let document = String::from("report.txt");
let open_btn = Button::new("Open", move || {
println!(" -> Opening {}", document);
});
save_btn.click();
open_btn.click();
open_btn.click();
}
Interactive Playground
The program below prints Above 3: [5, 8, 9, 4, 7] followed by eight transform lines. Three changes are worth making to it, and each has a stated expected result — predict before you run, then check yourself against the answer given:
- Change
thresholdto6. The filter line becomesAbove 6: [8, 9, 7]— three survivors instead of five. The transform lines do not change at all, becausetransformcapturesoffset, notthreshold. - Add
.take(3)immediately after.filter(...). The filter line becomesAbove 3: [5, 8, 9]. Note where the three came from:takeruns after the filter, so it keeps the first three values that passed, not the first three values ofdata. - Make the closure capture something mutably. Add
let mut seen = 0;abovetransformand rewrite it aslet transform = |x: i32| { seen += 1; x * 2 + offset };. Predict the compiler's reaction before running. It refuses, and the error is not about the closure's type — it iserror[E0596]: cannot borrow transform as mutable, as it is not declared as mutable, pointing at the call on theprintln!line. The note on the closure's own line explains why:calling transform requires mutable binding due to mutable borrow of seen. Because the body writes to a captured variable, the closure is anFnMut, and calling anFnMutneeds a mutable borrow of the closure itself — so the binding has to belet mut transform. rustc'shelp:here says exactly that and is worth taking: it adds amut, which changes the plumbing rather than the work. (Move theseen += 1;back out into the loop body instead and everything compiles, printingseen 8at the end — the loop's own code capturingseenis not the closure capturing it.)
fn main() {
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));
}
}
Try It Yourself
Reading about the three Fn traits is not the same as picking the right bound under your own name. This is a build task: a small program that reports its own pass/fail. Three functions are stubbed out — one takes a read-only closure, one takes a closure that updates captured state, and one returns a closure. Run the starter as-is and it fails immediately, naming the first check that did not pass. Implement each until every check passes and it prints All checks passed.
The signatures are already written, and they are the lesson in miniature: impl Fn where the closure only reads, impl FnMut (with mut f) where the caller's closure updates something it captured, and move on the returned closure because the factory's local dies at the return. You write only the bodies.
Build
Finish the build. Three functions are stubbed out and the checks below them fail until each one behaves. Run it as-is to see which check fails first, decide what that function is missing, then implement all three until it prints 'All checks passed.' The checks run top to bottom, so the first failure you see is TODO 1 — implement it first, then work down. Do not change the three signatures: each one's Fn bound is part of the exercise.
// A tiny event-processing kit. Each function takes or returns a closure.
// TODO 1: apply f to every element of values and collect the results.
// f only READS what it captures, so an Fn bound is enough.
// transform(&[1, 2], |n| n + 10) -> vec![11, 12]
fn transform(values: &[i32], f: impl Fn(i32) -> i32) -> Vec<i32> {
let _ = values;
let _ = f;
Vec::new()
}
// TODO 2: call f once per element of values, in order. Returns nothing.
// f is FnMut because the caller's closure will UPDATE captured state.
// Note the mut f in the signature — a FnMut closure must be called mutably.
fn for_each(values: &[i32], mut f: impl FnMut(i32)) {
let _ = values;
let _ = &mut f;
}
// TODO 3: return a closure that adds offset to whatever it is given.
// The closure outlives this function, so it must OWN offset — hence move.
// let add5 = adder(5); add5(2) -> 7
fn adder(offset: i32) -> impl Fn(i32) -> i32 {
let _ = offset;
move |n| n
}
// --- Build checks: these must all pass. Do not edit below this line. ---
fn main() {
let readings = [3, 8, 1, 6];
assert_eq!(transform(&readings, |n| n * 2), vec![6, 16, 2, 12], "transform should map every value through f");
let mut total = 0;
for_each(&readings, |n| total += n);
assert_eq!(total, 18, "for_each should call f once per value, letting it update captured state");
let bump = adder(10);
assert_eq!(bump(5), 15, "adder(10) should return a closure that adds 10");
assert_eq!(bump(0), 10, "the returned closure must be callable more than once");
println!("All checks passed.");
println!("Doubled: {:?}", transform(&readings, |n| n * 2));
println!("Total: {}", total);
println!("bump(7) = {}", bump(7));
}Expected output: All checks passed.
Doubled: [6, 16, 2, 12]
Total: 18
bump(7) = 17
Once it passes, try two variations and predict each before running:
- Weaken the bound on
for_each. Change its signature tofn for_each(values: &[i32], f: impl Fn(i32))— dropping both themutand theMut. Predict which check breaks before running. None do; it never gets that far. Compilation fails at the call site witherror[E0594]: cannot assign to total, as it is a captured variable in a Fn closure, and rustc helpfully points at the parameter with "change this to acceptFnMutinstead ofFn". The caller's closure did not change — the bound stopped permitting what it always did. - Drop the
movefromadder. Change its body to|n| n + offset. Decide what the compiler says before running. You geterror[E0373]: closure may outlive the current function, but it borrows offset, which is owned by the current function, with the suggestion to addmove. This is the returning-closures rule with teeth: the moment a closure escapes the function that built it, borrowing a local is no longer an option.
Put the Order Back
Here is the closure factory the next block's five lines use. It takes ownership of a prefix and returns a closure that has that prefix baked into it:
fn make_tagger(prefix: String) -> impl Fn(&str) -> String {
move |body| format!("{}{}", prefix, body)
}
Arrange the code
These five lines build a tagger, use it, and print the result shouted. Put them in the order that runs — then answer what the order is really testing: name what each line hands to the line below it, and say why the second line is the one that ends prefix's life in main.
let shouted = entry.to_uppercase();let entry = tag("started");let tag = make_tagger(prefix);let prefix = String::from("[log] ");println!("{}", shouted);
Carrying the Idea Across
Transfer
A Rust closure captures each variable with the least permission its body needs — shared borrow to read, mutable borrow to write, ownership if you write move. A JavaScript closure also captures variables from its surrounding scope, but by a different rule. Which statement names the genuine difference rather than a surface one?
Practice Exercises
-
Square closure: Write a closure that takes an
i32and returns its square. Use it with.map()on a vector of numbers. -
Custom filter: Create a function
filter_above(data: &[i32], min: i32) -> Vec<i32>that uses a closure internally to filter values abovemin. -
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: usemoveand a mutable variable.) -
Compose: Write a function
composethat takes two closuresfandgand returns a new closure that appliesf(g(x)). Test it by composing "double" and "add 1".
Key Takeaways
- Closures are anonymous functions defined with
|params| bodysyntax - 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
movekeyword forces ownership transfer into the closure - Three traits define how closures behave:
Fn(immutable borrow),FnMut(mutable borrow), andFnOnce(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.
Two-tier handoff: this document is the complete reading surface. Continue learning for stateful practice, progress, and real sandbox execution.