You can't put a shape-shifter in a box
You're building a task scheduler. You want to accept a block of code, save it in a struct, and run it five minutes later. You write struct Task { code: impl Fn() }. The compiler screams.
The error tells you that impl Fn is not a concrete type. You can't use impl Trait in a struct field. The compiler needs to know the exact size of Task to lay it out in memory, but closures are unique snowflakes. Every closure captures different variables, so every closure has a different size. You can't store a variable-sized thing in a fixed-size struct field.
The compiler doesn't care about your intent. It cares about size. Give it a pointer.
The size problem
Rust structs are like fixed-size shipping crates. When you define a struct, the compiler calculates the total size by adding up the sizes of every field. If you have struct Point { x: i32, y: i32 }, the compiler knows Point is always 8 bytes. It can allocate Point on the stack, pass it by value, and pack arrays of Point tightly.
Closures break this rule. A closure is an anonymous struct generated by the compiler. Its fields are the variables it captures from the environment.
fn make_counter() {
let count = 0;
let c1 = || count; // Captures nothing. Size: 0 bytes.
let name = String::from("Rust");
let c2 = || &name; // Captures a reference. Size: 8 bytes (pointer).
let data = vec![1, 2, 3];
let c3 = || data.len(); // Captures the Vec. Size: 24 bytes (Vec header).
}
c1, c2, and c3 all implement Fn() -> usize. They all look the same to the type system. But c1 is zero bytes, c2 is eight bytes, and c3 is twenty-four bytes. If you tried to store them in the same struct field, the compiler would have to pick a size. Picking the largest size wastes memory for small closures. Picking the smallest size truncates large closures. There is no safe way to pack variable-sized values into a fixed layout.
The solution is to move the closure off the stack and onto the heap. You store a pointer in the struct. The pointer has a fixed size. The closure can be any size you want.
The fix: Box and trait objects
You store closures using Box<dyn Fn(...)>. The Box allocates memory on the heap, moves the closure there, and returns a pointer. The dyn keyword creates a trait object. A trait object is a fat pointer: it points to the data on the heap and also points to a vtable that tells the program how to call the closure.
struct Callback {
// Store a pointer to the closure on the heap.
// The size of this field is always one pointer (8 bytes on 64-bit).
func: Box<dyn Fn()>,
}
fn main() {
let message = String::from("Hello");
// The closure captures `message`. Its size depends on the String.
let c = || println!("{}", message);
// Box::new allocates the closure on the heap.
// It returns a Box<dyn Fn()> which has a fixed size.
let callback = Callback { func: Box::new(c) };
// Call the closure through the pointer.
(callback.func)();
}
The Box wrapper handles the memory management. When callback goes out of scope, the Box drops, which decrements the reference count and frees the heap allocation containing the closure.
Convention aside: Write Box<dyn Fn()> with the empty parentheses. The () is optional in the type syntax, but including it makes it explicit that the closure takes no arguments. It saves readers from scanning the code to check if arguments were omitted by mistake.
The three faces of closures
Not all closures are created equal. Rust defines three traits for closures based on how they capture variables. You must pick the right trait when storing a closure, or the compiler will reject valid code.
Fn: The closure can be called multiple times. It borrows its captures immutably.FnMut: The closure can be called multiple times. It borrows its captures mutably.FnOnce: The closure can be called once. It moves its captures.
Fn is a subset of FnMut, which is a subset of FnOnce. A closure that implements Fn also implements FnMut and FnOnce. You can store an Fn closure in a Box<dyn FnOnce>, but you lose the ability to call it again through that box.
struct OnceTask {
// Store a closure that consumes itself.
// We use Option to allow taking the closure out.
task: Option<Box<dyn FnOnce()>>,
}
impl OnceTask {
/// Create a new task that runs exactly once.
fn new<F>(f: F) -> OnceTask
where
F: FnOnce() + 'static,
{
OnceTask { task: Some(Box::new(f)) }
}
/// Run the task if it hasn't been run yet.
fn run(&mut self) {
// Take the task out of the Option.
// If it's None, the task already ran.
if let Some(task) = self.task.take() {
task();
}
}
}
fn main() {
let mut task = OnceTask::new(|| println!("Boom"));
task.run(); // Prints "Boom".
task.run(); // Does nothing. The closure was consumed.
}
The Option pattern is essential for FnOnce. You can't call a FnOnce closure and keep it. Calling it moves the closure. If the struct owns the closure, you have to move the closure out of the struct to call it. Option::take swaps the Some with None and returns the value, leaving the struct in a valid state.
Treat FnOnce like a firework. You get one shot. If you need to reuse it, you're holding the wrong closure.
Real-world: Retry policy
Storing closures shines when you need to parameterize behavior without bloating your API. A retry policy is a classic example. You want to define a policy that checks whether to retry after an error. The check logic varies by use case, so you store a closure.
struct RetryPolicy {
max_attempts: u32,
// Store the check logic.
// The closure takes the attempt number and error message.
should_retry: Box<dyn Fn(u32, &str) -> bool>,
}
impl RetryPolicy {
/// Execute an operation with retry logic.
fn execute<F>(&self, mut operation: F)
where
F: FnMut() -> Result<(), String>,
{
for attempt in 1..=self.max_attempts {
match operation() {
Ok(_) => return,
Err(e) => {
// Call the stored closure to decide.
if !(self.should_retry)(attempt, &e) {
break;
}
}
}
}
}
}
fn main() {
// Define a policy: retry up to 3 times, but only on network errors.
let policy = RetryPolicy {
max_attempts: 3,
should_retry: Box::new(|attempt, error| {
attempt < 3 && error.contains("network")
}),
};
let mut calls = 0;
policy.execute(|| {
calls += 1;
Err("network timeout".to_string())
});
// The operation ran 3 times before giving up.
}
The closure captures nothing in this example, but it could capture configuration from the environment. The struct holds the policy, and the closure holds the logic. This keeps the struct definition clean and pushes complexity to the caller.
Pitfalls and errors
Storing closures introduces a few traps. Watch for these patterns.
Lifetimes leak into the struct.
If your closure captures a reference, the Box must carry the lifetime. This forces the struct to be generic over the lifetime.
struct Task<'a> {
// The closure borrows data with lifetime 'a.
func: Box<dyn Fn() + 'a>,
}
This makes the struct harder to use. Every function that takes Task must also handle 'a. If you want to store the struct in a collection or pass it across threads, the lifetime constraints become painful.
The compiler rejects code where the reference dies too early with E0597 (does not live long enough). If you hit this, check if the closure really needs to capture a reference. Often you can clone the data or use Rc to make the capture 'static.
Send and Sync bounds.
If you move the struct to another thread, the closure must be Send. If multiple threads read the struct, the closure must be Sync.
struct SharedTask {
// Add Send + Sync to allow sharing across threads.
func: Box<dyn Fn() + Send + Sync>,
}
If you forget these bounds and try to send the struct, the compiler rejects it with E0277 (the trait Send is not implemented). Closures that capture Rc are not Send. Closures that capture RefCell are not Sync. Check your captures.
Performance overhead.
Box allocation costs memory and time. Dynamic dispatch through dyn Fn involves a vtable lookup, which prevents inlining. If you store closures in a hot loop, the overhead adds up.
If performance matters, avoid Box<dyn Fn>. Use generics or function pointers.
Decision matrix
Pick the storage strategy based on your requirements.
Use Box<dyn Fn()> when you need to store a closure that captures environment and the struct has a single owner. This is the standard choice for callbacks and plugins.
Use Arc<dyn Fn()> when multiple owners need to share the closure, like in a concurrent system where threads clone the policy. Arc adds atomic reference counting for thread safety.
Use a plain function pointer fn() when the closure captures nothing and you want zero overhead and no allocation. Function pointers are just addresses. They are cheap and fast.
Use a generic struct struct Task<F: Fn()> { func: F } when you know the closure type at compile time and want the performance of monomorphization. Generics eliminate the vtable and allow inlining. The trade-off is code bloat and complex type signatures.
Use Option<Box<dyn FnOnce()>> when the closure consumes its captures and can only be called once. The Option wrapper lets you take the closure out and run it.
Use Box<dyn FnMut()> when the closure needs to mutate captured state across multiple calls. This is rare. Usually you wrap the state in Cell or RefCell and store an Fn closure instead.
If you can name the type, don't box it. Generics are free; boxes cost memory.