When a function needs to return a function
You are building a configuration system. You write a function that takes a log level and returns a handler. The handler captures the level and prints messages. In Python or JavaScript, you just return the lambda. Rust stops you. The compiler demands a return type. You try fn(i32), but that's a function pointer, and closures aren't function pointers. You try Box<dyn Fn>, but now you're allocating on the heap for no reason.
The core issue is that closures in Rust are not just functions. They are data structures that carry captured state. Every closure has a unique type, even if two closures look identical. Returning a closure means returning a value of a type that has no name you can write. Rust gives you two tools to solve this: impl Trait for concrete, zero-cost returns, and Box<dyn Trait> for dynamic, flexible returns.
Closures are structs in disguise
A closure is a function that captures variables from its environment. When you write |x| x + offset, the compiler doesn't just generate a function. It generates a unique, anonymous struct. That struct holds the captured variables as fields. The struct implements the Fn trait, which defines how the closure is called.
Think of a closure like a robot with a backpack. The robot knows how to perform a task. The backpack holds the supplies it needs. Every time you create a closure, Rust stamps a unique serial number on the backpack. Even if two backpacks contain the same items, they have different serial numbers. The compiler treats them as different types.
This unique typing is why you can't return a closure with a named type. The type exists, but it's opaque. impl Trait lets you return the value without naming the type. The compiler knows the exact type and its size. The caller only sees that the value implements the trait.
The minimal pattern: impl Fn
The standard way to return a closure is impl Fn(...) -> .... This syntax tells the compiler: "I am returning a concrete type that implements this trait. You figure out the exact type."
/// Creates a closure that adds the captured value to its argument.
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
// move is required because the closure outlives this function scope.
// The closure captures x by value so it owns the data.
move |y| x + y
}
fn main() {
let add_five = make_adder(5);
// The caller doesn't know the type of add_five.
// It only knows add_five implements Fn(i32) -> i32.
println!("{}", add_five(10)); // Output: 15
}
The move keyword is essential here. The closure is returned from the function, so it will exist after make_adder finishes. If the closure captures x by reference, the reference would dangle. move forces the closure to take ownership of x. The closure carries x in its backpack. When the closure is dropped, x is dropped.
The compiler generates a unique struct for the closure. That struct has a field for x. The struct implements Fn(i32) -> i32. The return type is that struct. The caller sees impl Fn(i32) -> i32. The caller can call the closure, but cannot inspect the struct fields. The size is known at compile time. The return happens on the stack. No heap allocation occurs.
The compiler knows the size. The caller doesn't need to.
How impl Trait works under the hood
impl Trait in return position is a concrete type. It is not dynamic dispatch. The compiler monomorphizes the code. Every call site gets its own copy of the function, specialized for the exact closure type.
When you call make_adder(5), the compiler generates a function that returns a specific closure struct. When you call make_adder(10), it generates the same function. The closure struct is the same type in both cases. The difference is the value inside the struct.
This is different from Box<dyn Fn>. A trait object uses dynamic dispatch. The compiler generates a vtable. Calls go through a pointer. impl Fn inlines the call. The performance is identical to calling a regular function.
Convention: prefer impl Fn over Box<dyn Fn> unless you need to store heterogeneous closures in a collection. The performance difference is real, and the ergonomics of impl Fn are better. impl Fn is the default choice for returning closures.
Realistic example: a stateful factory
Closures often need to mutate state. A rate limiter, for example, tracks how many calls have happened. The closure must mutate a counter. This requires FnMut instead of Fn. The closure also needs interior mutability if it's called multiple times.
use std::cell::RefCell;
/// Creates a rate limiter that allows max calls per invocation.
/// The returned closure tracks state internally.
fn make_rate_limiter(max: u32) -> impl Fn() -> bool {
// RefCell provides interior mutability.
// The closure captures count by move, so it owns the RefCell.
let count = RefCell::new(0);
// The closure is FnMut because it mutates count.
// However, the return type is impl Fn() -> bool.
// Rust coerces FnMut to Fn if the closure is only called once?
// No, this code has a subtle issue.
// A closure that mutates state is FnMut, not Fn.
// We must return impl FnMut() -> bool.
move || {
let mut c = count.borrow_mut();
*c += 1;
*c <= max
}
}
fn main() {
// This compiles with impl FnMut.
// If we used impl Fn, the compiler would reject the mutation.
let limiter = make_rate_limiter(3);
// Calling a FnMut closure requires mutability.
// We must call it on a mutable binding.
let mut limiter = limiter;
println!("{}", limiter()); // true
println!("{}", limiter()); // true
println!("{}", limiter()); // true
println!("{}", limiter()); // false
}
The return type must match the closure's capability. If the closure mutates captured variables, it implements FnMut. The return type must be impl FnMut(...). If the closure takes self by value, it implements FnOnce. The return type must be impl FnOnce(...).
The compiler enforces this hierarchy. Fn is the most restrictive. FnMut allows mutation. FnOnce allows consuming captures. A closure that implements Fn also implements FnMut and FnOnce. A closure that implements FnMut also implements FnOnce. The return type must be the most specific trait the closure satisfies.
Stateful closures are just structs with methods. RefCell gives you interior mutability when the signature demands it.
Pitfalls and compiler errors
Returning closures introduces specific errors. The compiler catches them early. Read the error codes. They point to the exact problem.
E0373: closure may outlive the current function
This error occurs when a closure captures a reference to a local variable and is returned. The local variable is dropped when the function ends. The closure would hold a dangling reference.
fn bad_capture() -> impl Fn() {
let data = String::from("hello");
// data is dropped here.
// The closure captures &data.
// The closure outlives data.
|| println!("{}", data)
}
The compiler rejects this with E0373. The fix is move. move forces the closure to take ownership of data. The closure carries the String. The data lives as long as the closure.
E0308: mismatched types
This error occurs when a function returns different closure types from different branches. Each closure has a unique type. The compiler cannot unify them.
fn inconsistent_return(x: bool) -> impl Fn() {
if x {
|| println!("true")
} else {
|| println!("false")
}
}
The compiler rejects this with E0308. The two closures have different types. They capture different strings. The fix is to use Box<dyn Fn> to erase the types, or restructure the code to return a single closure type.
E0277: trait bound not satisfied
This error occurs when a closure is used in a context that requires a trait it doesn't implement. Common cases are Send and Sync. If you return a closure to another thread, it must be Send.
use std::thread;
fn make_handler() -> impl Fn() {
// This closure captures nothing, so it is Send.
// But if it captured a Rc, it would not be Send.
|| println!("handler")
}
fn main() {
let handler = make_handler();
// Spawning a thread requires the closure to be Send + 'static.
// impl Fn() does not guarantee Send.
// The compiler rejects this.
thread::spawn(handler);
}
The compiler rejects this with E0277. The return type impl Fn() does not include Send. The fix is to return impl Fn() + Send. This constrains the closure to types that can be sent across threads.
The compiler protects you from type mismatches. Read the error. It tells you exactly which branches disagree.
Decision: when to use each pattern
Use impl Fn when you want zero-cost abstraction, stack allocation, and the caller doesn't need to store multiple different closures in a collection. The compiler generates concrete code. Calls are inlined. Performance is optimal.
Use impl FnMut when the closure mutates captured state. The return type must match the closure's capability. The caller must hold the closure in a mutable binding to call it.
Use impl FnOnce when the closure consumes captured values. The closure can only be called once. The return type must be impl FnOnce. The caller takes ownership of the closure to call it.
Use Box<dyn Fn> when you need to store heterogeneous closures in a Vec, or when the closure is created dynamically and you cannot use impl Trait. The trait object erases the type. Calls use dynamic dispatch. Heap allocation occurs. Use this only when flexibility is required.
Use move when the closure captures variables from the function scope and needs to outlive that scope. The closure takes ownership of the captures. This prevents lifetime errors and makes ownership clear.
Use impl Fn + Send when the closure must cross thread boundaries. The trait bound guarantees the closure can be sent safely. Without Send, the compiler rejects thread spawns.
Pick the tool that matches your lifetime and dispatch needs. impl Trait is fast. Box<dyn Trait> is flexible. Don't pay for flexibility you don't use.