Closures in Rust are anonymous functions that you can assign to variables, pass as arguments, and use just like regular functions. They can capture values from their environment, which makes them powerful for callbacks, iterators, and async workflows.
The first time I used a closure in Rust, it reminded me of JavaScript arrow functions, but stricter and safer. Unlike regular functions, Rust closures can remember variables from the scope where they were created, which makes them flexible and expressive. Once I understood how to define them, capture variables, and pass them into functions, they became one of my favourite tools in the language.
This post will teach you how closures work, how they differ from regular functions, and how to use them effectively in real Rust code.
What Is a Closure?
A closure is a small, anonymous function you can store in a variable or pass to another function.
It looks like this:
let add = |a, b| a + b;
You can call it like a normal function:
let sum = add(3, 4);
Closures are useful when you want to define short behavior inline, such as when filtering or mapping over data.
Syntax of Closures
Closures use the pipe syntax:
|param1, param2| expression
If the body is more than one expression, use braces:
let multiply = |a, b| {
let result = a * b;
result
};
Closures can also take no arguments:
let say_hi = || println!("Hi!");
You can call it with say_hi().
Capturing Environment Variables
One of the biggest differences between closures and regular functions is that closures can access variables from the surrounding scope.
let factor = 2;
let scale = |x| x * factor;
println!("{}", scale(5)); // prints 10
Here, the closure remembers the value of factor, even though it is not passed as an argument.
This is called capturing the environment.
Closures can capture by:
- Reference (read-only)
- Mutable reference
- Move (taking ownership)
Rust decides which to use based on how you use the closure.
Closures in Functions
You can pass closures to functions by using a generic type with a trait bound.
Here is a function that takes a closure and applies it:
fn apply<F>(f: F)
where
F: Fn(),
{
f();
}
You can now pass a closure to apply:
let hello = || println!("Hello");
apply(hello);
The Fn trait means the closure can be called like a function and does not take ownership of anything.
Closure Traits
Rust defines three main traits for closures:
- Fn: read-only access (most common)
- FnMut: can mutate captured variables
- FnOnce: takes ownership of captured variables
You choose the trait based on how the closure interacts with its environment.
For example:
let mut count = 0;
let mut increment = || {
count += 1;
println!("{}", count);
};
increment(); // needs FnMut
Here, the closure needs FnMut because it changes count.
Type Inference with Closures
Rust usually infers the argument and return types of closures:
let square = |x| x * x;
But you can add explicit types if needed:
let square = |x: i32| -> i32 {
x * x
};
This is helpful when passing closures to functions or writing complex logic.
Closures in Iterators
Closures are often used with iterators, especially with methods like map, filter, and for_each.
Example:
let numbers = vec![1, 2, 3];
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
Here, the closure |x| x * 2 is passed to map, and it doubles each number in the list.
We will explore more of this in the upcoming post on The Iterator Trait and .next().
Closures vs Regular Functions
Feature | Closures | Regular Functions |
---|---|---|
Named? | No | Yes |
Can capture scope? | Yes | No |
Can infer types? | Yes | No |
Flexible for reuse? | Yes (great for callbacks) | Yes (great for modular logic) |
Use closures for short, inline logic that depends on surrounding variables. Use regular functions for reusable, global behavior.
Summary
Closures in Rust are anonymous, inline functions that can capture variables from their environment. They are perfect for passing into iterators, defining callbacks, and creating flexible, short behavior. You can store them in variables, pass them around, and control how they capture data using Fn, FnMut, or FnOnce.