What Is a “Handler” in Rust?

Handlers
Understanding Handlers, Dispatch Tables, and Function Pointers
If you’re learning Rust and you keep seeing the word handler, here’s the plain truth:
A handler is just a function that gets called in response to something happening.
That “something” could be:
- a command name
- an opcode
- an HTTP route
- a button click
- an MCP tool invocation
The key idea is you don’t call the handler directly.
Some other part of the program (a framework, router, or engine) chooses which handler to call and then calls it for you.
A Minimal Handler Example
Let’s start with a very simple Rust example:
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn oper(a: i32, b: i32, op: fn(i32, i32) -> i32) -> i32 {
op(a, b)
}
fn main() {
let result = oper(2, 3, add);
println!("{}", result);
}
Here’s what’s really happening:
addis a handleroperis a dispatcheropis a function pointer (a pointer to executable code)
The dispatcher doesn’t care what the handler does.
It only cares that it can call it with (i32, i32) and get back an i32.
That separation is the foundation of handler-based design.
Why This Pattern Exists
Instead of writing logic like this:
if command == "add" {
add(a, b)
} else if command == "sub" {
sub(a, b)
}
We move the decision into data, not code.
This leads us to the next concept.
Dispatch Tables: Where Handlers Live
A dispatch table is just a mapping from some key to a handler.
use std::collections::HashMap;
type Handler = fn(i32, i32) -> i32;
fn add(a: i32, b: i32) -> i32 { a + b }
fn sub(a: i32, b: i32) -> i32 { a - b }
fn main() {
let mut table: HashMap<&str, Handler> = HashMap::new();
table.insert("add", add);
table.insert("sub", sub);
let command = "add";
let result = table[command](10, 4);
println!("{}", result);
}
Now the program says:
“When I see the command
add, call theaddhandler.”
This is exactly how:
- HTTP routers
- CLI command parsers
- protocol decoders
- MCP tool registries
work internally.
Why Use fn for Handlers?
Using fn means:
- no captured state
- no heap allocation
- simple, uniform types
- easy storage in tables
- FFI and ABI friendly
That’s why low-level systems, servers, and runtimes still use function-pointer handlers today.
If you later need state or closures, you can upgrade to Fn or Box<dyn Fn>.
But conceptually, it’s still a handler.
The One-Sentence Mental Model
A handler is the function that runs when a specific command, event, or tool is selected — and a dispatch table decides which one gets called.
Once you understand that, a huge amount of Rust infrastructure suddenly makes sense.
Dispatch tables are everywhere:
- Web servers → map URLs to route handlers
- Games → map button presses to actions
- MCP / command systems → map tool names to tool functions
- Interpreters → map opcodes to operations
Basically, any time your program sees “something happened” and needs to pick a function to run, that’s a dispatch table.
The function name without parentheses is the pointer/reference to the function itself
When you do this in Actix or Axum:
// Axum
let app = Router::new()
.route("/users", get(get_users)) // ← function pointer!
.route("/login", post(login)); // ← function pointer!
// Actix
HttpServer::new(|| {
App::new()
.route("/users", web::get().to(get_users)) // ← function pointer!
})
You’re passing function pointers to the framework. The framework stores these and calls them later when a request comes in.
The flow:
- Setup time: You pass
get_users(without()) → framework stores the pointer - Request time: HTTP request arrives → framework calls
get_users()(with())
Why this matters:
- The framework needs to control when to call your handler (when a request arrives)
- It needs to pass the right arguments (
Request, extractors, etc.) - If you wrote
get(get_users()), it would call your function immediately during setup, not when requests arrive!
So yes, every handler registration in web frameworks is using function pointers under the hood.
