When a function is too much ceremony
You are building a task manager. You have a list of tasks. You need to filter them: show only urgent ones, show only completed ones, show only tasks due today. Writing a separate named function for every filter feels repetitive. You want to pass the filtering logic itself as an argument. That is where closures come in. They let you write a tiny function right where you need it and hand it off to other code.
What a closure actually is
A normal function is defined once and lives independently. It knows its name and its parameters. A closure is an anonymous function that captures its environment. Think of a closure like a robot vacuum that remembers the layout of your house. The vacuum is the code. The map of the house is the captured environment. When the vacuum moves to a new room, it still knows where the walls are because it captured that context when it started. In Rust, a closure captures variables from the surrounding scope so it can use them later.
Under the hood, Rust closures are not magic. The compiler generates a unique, unnameable struct for every closure. This struct holds the captured variables as fields. The struct implements a trait that allows you to call it like a function. This is why closures have a concrete type. You cannot name the type in your code, but the compiler knows exactly what it is.
Minimal example
Closures use the |args| body syntax. The pipes delimit the arguments. The body is an expression that returns a value.
fn main() {
let base = 10;
// This closure captures `base` from the environment.
// It does not take `base` as an argument.
let add_base = |n| n + base;
// Invoke the closure by storing it in a variable and calling it.
let result = add_base(5);
println!("{}", result); // 15
}
The closure add_base captures base. When you call add_base(5), the closure uses the captured value. The compiler generates a struct with a field for base and a field for the logic. The call passes 5 to the logic, which adds the stored base.
The closure captures the minimum needed. It will capture by reference if reading is enough. It will capture by mutable reference if it needs to mutate. It will capture by value only if it needs to consume the variable. This greedy capture behavior saves memory and avoids unnecessary moves.
The closure captures the minimum. It won't take ownership unless it has to.
How capture modes work
Rust closures inspect the body to decide how to capture each variable. This happens at compile time.
fn main() {
let mut data = vec![1, 2, 3];
// This closure mutates `data`, so it captures `&mut data`.
// It cannot be called concurrently because `&mut` is exclusive.
let mut pusher = || {
data.push(4);
};
// Call the closure.
pusher();
println!("{:?}", data); // [1, 2, 3, 4]
}
Here the closure mutates data. The compiler captures &mut data. Because the capture is mutable, the closure itself must be mutable (let mut pusher). You cannot call a &mut capture closure from multiple places at once. The borrow checker enforces this.
If the closure only reads data, the compiler captures &data. The closure can be called multiple times and shared freely.
If the closure consumes data, the compiler captures data by value. The closure can only be called once. After the call, the data is gone.
Capture modes are greedy. The closure takes what it needs, nothing more.
Real-world usage: Iterators
Closures shine with iterators. Iterators provide methods like filter, map, and fold that take closures as arguments. You pass the logic, the iterator handles the loop.
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// Use a closure to filter and transform.
let evens_doubled: Vec<i32> = numbers
.iter()
// Filter keeps items where the closure returns true.
.filter(|&n| n % 2 == 0)
// Map applies the closure to each item.
.map(|n| n * 2)
.collect();
println!("{:?}", evens_doubled); // [4, 8]
}
The filter method takes a closure that returns bool. The map method takes a closure that transforms the item. The convention is to use inline closures here. Do not write a named function for a one-liner. The closure syntax keeps the logic close to the data pipeline.
The compiler inlines closures aggressively. There is no runtime cost for the closure call itself. The generated code is often faster than a manual loop because the compiler can optimize across the closure boundary.
Iterators and closures are a pair. Use them together to write declarative data pipelines.
The move keyword
By default, closures capture by reference. This works fine when the closure lives shorter than the captured variables. Problems arise when the closure outlives the scope.
fn main() {
let s = String::from("hello");
// This closure captures `&s`.
let c = || println!("{}", s);
// If we drop `s` here, the closure holds a dangling reference.
// The compiler prevents this.
drop(s);
c(); // Error: use of moved value or borrow issue.
}
The compiler rejects this. The closure borrows s. You cannot drop s while the borrow is active. If you need the closure to own the data, use the move keyword.
fn main() {
let s = String::from("hello");
// `move` forces the closure to take ownership of `s`.
let c = move || println!("{}", s);
// Now `s` is moved into `c`. You cannot use `s` here.
// println!("{}", s); // Error E0382: use of moved value.
// The closure owns the data. It can outlive the original scope.
c();
}
The move keyword changes the capture mode. The closure captures every variable by value. This transfers ownership into the closure. The original variables become unavailable. This is essential when spawning threads or storing closures in structs that outlive the current function.
Use move to transfer ownership. The closure becomes the new owner.
Pitfalls and errors
Every closure has a unique type. Two identical closures have different types. This breaks function signatures.
// This won't compile. The compiler cannot name the closure type.
// fn apply(f: ???) { f(); }
You cannot write a function that takes a closure without using generics or trait objects. The compiler generates a unique struct for each closure, and you cannot name that struct.
// Use a generic parameter with a trait bound.
fn apply<F>(f: F) where F: Fn() {
f();
}
fn main() {
let c = || println!("Hello");
apply(c);
}
The generic F accepts any type that implements Fn(). The compiler substitutes the unique closure type for F. This works because the trait bound describes the behavior, not the type.
If you forget the trait bound, the compiler rejects the code with E0277 (the trait bound is not satisfied). The error tells you that the type does not implement the required trait.
Every closure has a unique type. You need generics to accept them.
Decision: When to use closures
Use a closure when you need a short snippet of logic to pass to an iterator, a callback, or a higher-order function. Use a named function when the logic is complex, reused across modules, or needs to be stored in a field with a known type. Use the move keyword when the closure must outlive the current scope, such as when spawning a thread or storing the closure in a struct. Use generics with Fn bounds when writing a function that accepts a closure, allowing the caller to pass any compatible closure type. Use Box<dyn Fn> when you need to store heterogeneous closures in a collection or return a closure from a function where the concrete type is unknown.