Closures
Reviewed & published by Brayan K
Rust is a systems programming language focused on speed, memory safety, and fearless concurrency — and closures are anonymous functions that can capture variables from the scope around them.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll write closures, capture the environment, pass and return them with the Fn traits, and use move.
What You'll Learn in This Lesson
- Closure syntax with | parameters |
- Capturing variables from the environment
- Passing closures into functions
- Returning closures with impl Fn
- The Fn, FnMut, and FnOnce traits
- When and why to use move
1️⃣ Closure Basics and Capturing
A closure is written with its parameters between vertical bars, like |a, b| a + b. Types are usually inferred. The defining feature is that a closure captures variables from the surrounding scope — so it can use values that aren't passed in as parameters.
fn main() {
// A closure is an anonymous function. Parameters go between | bars |.
let add = |a: i32, b: i32| a + b;
println!("add: {}", add(2, 3)); // -> add: 5
// Types are usually inferred, so you can often omit them.
let double = |x| x * 2;
println!("double: {}", double(10)); // -> double: 20
// The big difference from fn: closures CAPTURE their environment.
let factor = 3;
let scale = |x| x * factor; // captures factor from the surrounding scope
println!("scale: {}", scale(5)); // -> scale: 15
// A multi-line closure uses braces.
let describe = |n: i32| {
let kind = if n % 2 == 0 { "even" } else { "odd" };
format!("{} is {}", n, kind)
};
println!("{}", describe(7)); // -> 7 is odd
}The scale closure used factor even though it was never a parameter — it captured it from the enclosing scope. A plain fn can't do that.
2️⃣ Passing, Returning, and the Fn Traits
Closures are values you can pass to and return from functions. A parameter typed impl Fn(i32) -> i32 accepts any read-only closure of that shape. To return a closure that outlives the function, use move so it owns its captures. Mutating closures use FnMut.
// Closures are commonly passed to other functions. A parameter typed
// "impl Fn(i32) -> i32" accepts any closure taking an i32 and returning one.
fn apply(f: impl Fn(i32) -> i32, value: i32) -> i32 {
f(value)
}
// A function can also RETURN a closure. "move" makes the closure take
// ownership of captured variables so it can outlive this function.
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
fn main() {
let squared = apply(|x| x * x, 5);
println!("squared: {}", squared); // -> squared: 25
let add_ten = make_adder(10);
println!("add_ten(7): {}", add_ten(7)); // -> add_ten(7): 17
// FnMut closures can mutate captured state across calls.
let mut count = 0;
let mut increment = || { count += 1; count };
println!("{} {} {}", increment(), increment(), increment()); // -> 1 2 3
}make_adder returned a closure that captured n by value (thanks to move), so it stays valid after the function returns. The increment closure mutated count across calls — that's FnMut.
Your turn. Fill in the blanks marked ___, then run it.
// 🎯 YOUR TURN — finish the closures, then run it.
fn main() {
// 1) A closure that triples its input. Fill in the body.
let triple = |x: i32| x ___ 3; // 👉 the operator to multiply
println!("triple(4) = {}", triple(4)); // expected: triple(4) = 12
// 2) A closure that captures bonus from the surrounding scope.
let bonus = 100;
let with_bonus = |score| score + ___; // 👉 the captured variable
println!("with_bonus(50) = {}", with_bonus(50)); // expected: 150
}
// ✅ Expected output:
// triple(4) = 12
// with_bonus(50) = 150Mini-Challenge: A Closure Factory
Write a function that returns a multiplying closure with move. Run it with cargo run.
// 🎯 MINI-CHALLENGE: a closure factory.
//
// 1. Write fn multiplier(factor: i32) -> impl Fn(i32) -> i32 that returns a
// closure multiplying its argument by factor. Use "move" so the closure
// owns factor.
// 2. In main, create a "double" closure with multiplier(2) and a "triple"
// closure with multiplier(3).
// 3. Print double(5) and triple(5).
//
// ✅ Example output:
// double(5) = 10
// triple(5) = 15
fn main() {
// your code here
}Common Errors (and the fix)
- "closure may outlive the current function" — When returning a closure that captures locals, add move so it owns them.
- "cannot borrow `x` as mutable" — A closure that mutates a capture needs the variable (and the closure binding) to be mut.
- "expected closure ... found a different closure" — Every closure has a unique type. Return them via impl Fn or Box<dyn Fn>, not a concrete name.
- "use of moved value" — After a move closure captures a value, the original is gone. Clone first if you still need it.
Pro Tips
- 💡 Let types infer: closures rarely need type annotations — write |x| x * 2 and let Rust figure it out.
- 💡 Use impl Fn for parameters and returns: it's the cleanest way to accept or hand back a closure.
- 💡 Reach for move with threads: closures sent to another thread almost always need move.
📋 Quick Reference — Closures
| Goal | Rust Syntax |
|---|---|
| Closure | let f = |x| x + 1; |
| Multi-line | |x| { let y = x; y } |
| Take a closure | f: impl Fn(i32) -> i32 |
| Return a closure | -> impl Fn(i32) -> i32 |
| Own captures | move |x| x + n |
| Mutating | let mut f = || count += 1; |
🎉 Lesson Complete!
- ✅ Closures are anonymous functions written with | | that capture their environment
- ✅ Pass them with impl Fn(...); return them with impl Fn(...) -> ...
- ✅ Fn reads, FnMut mutates, FnOnce consumes captures
- ✅ move makes a closure own its captures so it can outlive its scope
Practice quiz
What defining ability does a closure have that a plain fn does not?
- It can be recursive
- It can capture variables from the surrounding scope
- It runs faster
- It returns multiple values
Answer: It can capture variables from the surrounding scope. Closures capture variables from their environment; plain functions only see parameters and globals.
How are a closure's parameters written?
- In parentheses ( )
- Between vertical bars | |
- In brackets [ ]
- After the fn keyword
Answer: Between vertical bars | |. Closure parameters go between vertical bars, e.g. |a, b| a + b.
What does this print? let factor = 3; let scale = |x| x * factor; println!("{}", scale(5));
- 3
- 5
- 15
- 8
Answer: 15. The closure captures factor (3) and multiplies: 5 * 3 = 15.
Which trait does a closure implement if it only reads its captured values?
- Fn
- FnMut
- FnOnce
- Copy
Answer: Fn. A read-only closure implements Fn and can be called many times.
Which trait describes a closure that mutates a captured value?
- Fn
- FnMut
- FnOnce
- Drop
Answer: FnMut. FnMut closures mutate captured state and require mutable access to be called.
Which trait describes a closure that consumes (moves out) a captured value, callable at most once?
- Fn
- FnMut
- FnOnce
- Iterator
Answer: FnOnce. FnOnce closures take ownership of a capture, so they can be called only once.
What does this print? let mut count = 0; let mut inc = || { count += 1; count }; println!("{} {} {}", inc(), inc(), inc());
- 0 1 2
- 1 2 3
- 3 3 3
- 1 1 1
Answer: 1 2 3. The FnMut closure increments and returns count each call: 1, then 2, then 3.
What does the move keyword do to a closure's captures?
- Borrows them
- Forces the closure to take ownership of them
- Copies them lazily
- Deletes them
Answer: Forces the closure to take ownership of them. move makes the closure own its captures so it can outlive the scope it was created in.
What does this print? fn make_adder(n: i32) -> impl Fn(i32) -> i32 { move |x| x + n } let add_ten = make_adder(10); println!("{}", add_ten(7));
- 10
- 7
- 17
- 70
Answer: 17. make_adder(10) returns a closure adding 10, so add_ten(7) is 17.
When is move almost always required?
- For one-line closures
- When sending a closure to another thread
- When the closure takes no parameters
- When using type inference
Answer: When sending a closure to another thread. Closures sent to another thread must own their captures, so they need move.
Continue this course
- Previous: Lifetimes
- Next: Iterators — Process sequences lazily with map, filter, collect, and the Iterator trait
- Quick reference: Rust cheat sheet
Frequently asked questions
What is the difference between a closure and a regular function?
The defining difference is that a closure can capture variables from the scope in which it is defined, while a plain fn cannot. A closure written as |x| x + factor automatically pulls factor in from the surrounding code. Closures also have lighter syntax — parameters go between vertical bars and types are usually inferred — and they are typically used inline, for example as arguments to iterator methods. A function is a named, standalone definition that only sees its parameters and globals.
What are the Fn, FnMut, and FnOnce traits?
They describe how a closure uses its captured environment. Fn closures only read captured values, so they can be called many times and shared. FnMut closures mutate captured values, so calling them requires mutable access. FnOnce closures consume (move out) a captured value, so they can be called at most once. Every closure implements one or more of these automatically based on what it does, and functions that accept closures specify which trait they need — for example, impl Fn(i32) -> i32 for a read-only closure.
When do I need the move keyword?
The move keyword forces a closure to take ownership of the variables it captures rather than borrowing them. You need it when the closure must outlive the scope where it was created — for example, when you return a closure from a function or send it to another thread. Without move, the closure would hold references that become invalid once the surrounding function returns, which the compiler rejects. With move, the captured values are owned by the closure and travel with it.
How do closures work with iterators?
Closures are the natural partner of iterator methods. Methods like map, filter, and for_each each take a closure that describes what to do with each element — for instance numbers.iter().map(|x| x * 2). The closure can capture surrounding variables, which makes these chains concise and expressive. This pairing of closures and iterators is one of the most idiomatic and powerful patterns in Rust, and it is the focus of the very next lesson.