Introduction: Why Callbacks in Rust Are a Challenge (and an Opportunity)
\nIf you've ever written code in JavaScript, Python, or C#, callbacks are a familiar tool for you. You pass a function that will be called later, and you live peacefully. But when you come to Rust, this simple pattern suddenly turns into a minefield: ownership, borrowing, lifetimes, dyn vs impl...
On Stack Overflow, the question "Idiomatic callbacks in Rust" has gathered thousands of views. This means even experienced developers stumble on this topic. But the good news is: Rust offers not one, but several elegant ways to implement callbacks. And today we'll break them all down — from simple closures to powerful trait objects and enum patterns.
\nThis article is for you if you:
\n- \n
- Have started learning Rust and want to understand how to properly organize asynchronous and event-driven architectures; \n
- Are transitioning from other languages and looking for idiomatic equivalents to familiar patterns; \n
- Want to write clean, safe, and performant code in Rust. \n
Let's go!
\n\nThe Problem: What's Wrong with Simple Callbacks?
\nImagine we are writing a simple task scheduler. In Python, it would look like this:
\ndef schedule(callback):\n # do something\n callback(result)\nIn Rust, the first attempt might be:
\nfn schedule(callback: fn(i32) -> i32) {\n let result = 42;\n callback(result);\n}\nAnd this works! But only for functions, not closures. Try passing a closure that captures a variable:
\nlet factor = 2;\nlet closure = |x| x * factor;\nschedule(closure); // Error! expected fn pointer, found closure\nThe compiler rightly complains: fn is a function pointer, but a closure is an anonymous type that can capture its environment. We need a more flexible approach.
Approach 1: Closures with impl Fn (Simplicity and Flexibility)
\nThe simplest and most commonly used way is to use a generic type with the Fn, FnMut, or FnOnce trait.
When to use Fn, FnMut, FnOnce?
\n- \n
FnOnce— if the callback is called exactly once (consumes captured values); \n FnMut— if the callback can modify captured state (called multiple times); \n Fn— if the callback does not modify state (pure function). \n
Example with impl Fn:
fn schedule<F>(callback: F)\nwhere\n F: Fn(i32) -> i32,\n{\n let result = 42;\n println!("Result: {}", callback(result));\n}\n\nfn main() {\n let factor = 2;\n let closure = |x| x * factor;\n schedule(closure); // Works!\n \n // You can also pass a regular function\n fn double(x: i32) -> i32 { x * 2 }\n schedule(double);\n}\nPros: maximally flexible, supports closures, safe, readable.
\nCons: monomorphization — for each closure type, the compiler generates a separate version of the function. If there are many callbacks, code can bloat.
Approach 2: Trait Objects Box<dyn Fn> (Dynamic Dispatch)
\nIf you want to store callbacks in collections (e.g., in a Vec) or pass them between threads, generics won't work — you need a single type. Trait objects come to the rescue:
type Callback = Box<dyn Fn(i32) -> i32>;\n\nstruct Scheduler {\n callbacks: Vec<Callback>,\n}\n\nimpl Scheduler {\n fn new() -> Self {\n Scheduler { callbacks: Vec::new() }\n }\n \n fn register(&mut self, callback