A word you've been reading without defining
If you've followed this series — or any of our other series — you've read the word "handler" dozens of times by now. BLoC event handlers. Method call handlers. FFI callback handlers. Domain event handlers. Completion handlers. Error handlers.
Every time, the meaning was clear enough in context that you didn't stop. You understood what it did. You moved on.
But try to define it, and it gets slippery. What is a handler? What makes it different from any other function? Why does the word appear in completely different contexts — UI events, native interop, domain logic, HTTP middleware — and somehow mean something coherent across all of them?
It turns out there's a precise answer. And once you see it, the word stops being vague and starts being a structural concept you can reason about.
The definition, stated plainly
A handler is a function whose purpose is to respond to something that has already been decided.
The decision happened somewhere else. A user tapped a button. An event was emitted. A network packet arrived. A timer expired. A C library finished processing a buffer. Something, somewhere, determined that an action should now be taken — and the handler is the function that takes it.
This is the key distinction. A handler doesn't decide whether to act. It doesn't listen for things. It doesn't route requests. It doesn't define the shape of data. It executes. It's the function at the end of a chain, the one that does the work.
If programming were a restaurant: the host decides which table you sit at. The waiter listens for your order. The menu defines what's available. The chef handles the order — takes the inputs, does the work, produces the output.
What a handler is not
The definition becomes sharper when you see what it excludes.
A handler is not a listener.
A listener's job is to wait. It subscribes to a stream, watches a port, monitors a change. When the thing it's watching produces something, the listener fires — and typically calls a handler.
// The listener — watches for changes
textController.addListener(() {
// The handler — responds to the change
_handleTextChanged(textController.text);
});The listener is the ear. The handler is the response.
In Flutter, StreamSubscription is a listener. The function you pass to .listen() is the handler. GestureDetector is a listener for touch events. The onTap callback is the handler. The distinction is not academic — when you cancel a StreamSubscription, you're removing the listener, which means the handler stops being called. The handler itself didn't change; the mechanism that delivered events to it was removed.
A handler is not a controller.
A controller's job is to coordinate. It receives input, decides what to do with it, and dispatches to the right place. In NestJS, a controller receives an HTTP request and routes it to the right service method. In Flutter, a TextEditingController manages the state of a text field — it doesn't process the text, it holds it and notifies listeners when it changes.
The controller decides what happens. The handler does the thing that happens.
A handler is not a model.
A model defines the shape of data. A User class, a PaymentResult enum, a CreateOrderDto. These are nouns, not verbs. A handler takes a model as input, does something with it, and possibly produces another model as output.
Handlers in Flutter: the pattern everywhere
Once you see the pattern, you'll recognize it in every part of Flutter development. Let's walk through the contexts where "handler" appears — many of which you've already encountered in our other posts.
UI event handlers
The most familiar kind. Every onTap, onPressed, onChanged, onSubmitted callback in Flutter is a handler:
ElevatedButton(
onPressed: _handleSubmit, // ← handler
child: Text('Submit'),
)The button widget is the listener — it detects the press gesture. Your _handleSubmit function is the handler — it runs when the press is confirmed. The framework chose to name the parameter onPressed rather than pressHandler, but the role is the same.
The Flutter convention of prefixing these with handle or _handle isn't mandatory, but it's widespread — and it's documentation built into the name. When you see _handleFormSubmit, you immediately know: this function responds to form submission. It doesn't listen for it, manage it, or define it. It does the work.
BLoC event handlers
If you've read our BLoC and GetIt guide, you've seen this pattern:
class PostBloc extends Bloc<PostEvent, PostState> {
PostBloc(this._getPostsUseCase) : super(PostInitial()) {
on<FetchPostsRequested>(_onFetchRequested); // ← registering the handler
}
Future<void> _onFetchRequested( // ← the handler itself
FetchPostsRequested event,
Emitter<PostState> emit,
) async {
emit(PostLoading());
final posts = await _getPostsUseCase();
emit(PostLoaded(posts));
}
}The on<FetchPostsRequested>() call is the registration — it tells the BLoC "when this event type arrives, call this function." The _onFetchRequested method is the handler. It doesn't decide whether to run. It doesn't listen for events. When a FetchPostsRequested event arrives, the BLoC framework calls it, and it does the work: emit loading state, fetch data, emit result.
Notice the structure: the handler receives the event (the thing that happened) and the emitter (the tool to produce output). It's purely reactive. Something happened; now deal with it.
Domain event handlers
In the domain events post, handlers take on a richer role:
class InventoryDecrementHandler {
final InventoryRepository _repository;
Future<void> handle(OrderConfirmedEvent event) async {
for (final item in event.items) {
await _repository.decrementStock(item.productId, item.quantity);
}
}
}An order was confirmed. The OrderConfirmedEvent was published. The InventoryDecrementHandler responds — it decrements stock. It didn't decide that the order should be confirmed. It didn't listen for orders. An event bus delivered the event, and the handler did its job.
The power of this pattern: when loyalty points were added to the system, a new LoyaltyPointsHandler was registered. The order aggregate wasn't touched. The event already existed. The handler is the extension point.
FFI callback handlers
In our FFI callbacks post, we showed the reverse direction — C calling Dart:
void _onAudioBufferReady(Pointer<Float> buffer, int length) {
// Process the audio data that C just produced
final samples = buffer.asTypedList(length);
_processAudio(samples);
}A C audio library doesn't wait for you to ask for data. It calls your function when a buffer is ready. Your function — the handler — responds to that call. The C library is the event source. The FFI callback mechanism is the delivery system. Your Dart function is the handler.
Method channel handlers
In our method channels post, the native side registers a handler for calls coming from Dart:
// iOS — Swift
paymentChannel.setMethodCallHandler { [weak self] call, result in
switch call.method {
case "initialize":
self?.handleInitialize(result: result) // ← handler
case "startPayment":
self?.handleStartPayment(call: call, result: result) // ← handler
default:
result(FlutterMethodNotImplemented)
}
}The setMethodCallHandler registers the routing logic — a switch that dispatches to specific handlers based on the method name. Each handle* method is a handler in the purest sense: a function that receives a request and does the work.
HTTP middleware handlers
Even outside Flutter, in server frameworks, the pattern is identical. A Dio interceptor:
onRequest: (options, handler) async {
options.headers['Authorization'] = 'Bearer $token';
handler.next(options); // ← pass to the next handler in the chain
}Here, handler is literally the parameter name. Each interceptor in the chain is a handler — it receives the request, does its work (adds a header, logs something, checks auth), and passes it along. The chain itself is the routing mechanism. Each handler is one step.
The handler's contract
Across all these examples — UI events, BLoC, domain events, FFI callbacks, method channels, HTTP interceptors — a pattern emerges. Every handler follows the same implicit contract:
- It receives input it didn't ask for. An event, a request, a callback invocation. The handler didn't go looking for this data. It was delivered.
- It executes a specific task. Decrement stock. Emit a state. Process audio. Add a header. The scope is focused.
- It doesn't manage its own lifecycle. A handler doesn't decide when to start listening or when to stop. Something else — a listener, a framework, an event bus — manages when the handler gets called. The handler exists to be called.
- It's replaceable. Because a handler is just a function that conforms to a contract (takes this input, produces this output), you can swap one handler for another without changing the system that invokes it. This is why BLoC event handlers are testable in isolation, why domain event handlers can be added without modifying aggregates, and why method channel handlers can be mocked in tests.
Handlers and SDKs: a brief clarification
You might wonder: do SDKs provide handlers?
Yes — but an SDK is not a handler. An SDK is a toolkit: a package of functions, classes, configuration, and documentation that makes integration with a service or platform easier. An SDK contains handlers, among many other things.
Firebase Cloud Messaging gives you FirebaseMessaging.onMessage — a stream you listen to. The function you pass to .listen() is your handler. The SDK gave you the listener mechanism and the message type. You provide the handler — the function that decides what your app does when a notification arrives.
The Stripe SDK provides payment sheet handlers, error handlers, completion handlers. These are functions you write that the SDK calls at the right moment. The SDK provides the infrastructure. You provide the response.
This is a useful way to think about any SDK integration: the SDK manages the complexity of when things happen. Your handlers define what happens.
Why the word is everywhere
"Handler" appears in so many different contexts because the pattern appears in so many different contexts. Any system that separates "something happened" from "here's what to do about it" needs handlers. That separation is fundamental to event-driven programming, reactive architectures, plugin systems, middleware chains, and callback-based APIs.
Flutter is deeply event-driven. Taps are events. State changes are events. Navigation is event-driven. BLoC is explicitly an event-driven architecture. FFI callbacks are events from native code. Method channels are events across the platform boundary. Domain events are events in the business logic layer.
Every one of these needs a function that responds. That function is a handler.
The reason the word feels vague is that it describes a role, not a specific implementation. A handler can be a method, a closure, a class with a handle() method, a static function, a lambda. The implementation varies. The role is constant: receive input, do the work, done.
The spectrum of responsibility
Not all handlers are equal in complexity. It helps to think of a spectrum:
Thin handlers — do one thing, immediately:
void _handleClear() {
textController.clear();
}Coordinators — do one thing, but it involves multiple steps:
Future<void> _onFetchRequested(
FetchPostsRequested event,
Emitter<PostState> emit,
) async {
emit(PostLoading());
try {
final posts = await _getPostsUseCase();
emit(PostLoaded(posts));
} catch (e) {
emit(PostError(e.toString()));
}
}Rich domain handlers — enforce business rules as they respond:
Future<void> handle(OrderConfirmedEvent event) async {
for (final item in event.items) {
final currentStock = await _repository.getStock(item.productId);
if (currentStock < item.quantity) {
await _alertService.notifyBackorder(item.productId);
}
await _repository.decrementStock(item.productId, item.quantity);
}
}The handler's scope should match the responsibility. A UI handler that orchestrates three API calls, two state emissions, and a navigation action is doing too much — it's becoming a controller. A domain handler that only forwards to another handler is doing too little — it's ceremony without purpose.
The general rule: a handler should be the smallest unit of response that makes sense for the context. In BLoC, that's one event type to one handler. In domain events, that's one side effect per handler. In UI, that's one user action to one response.
Naming conventions
Flutter and Dart have a loose but recognizable convention:
- UI handlers:
_handleTap,_handleSubmit,_handleTextChanged— prefixed with_handle, private to the widget - BLoC handlers:
_onEventName— prefixed with_on, registered in the constructor - Domain handlers:
SomethingHandlerwith ahandle()method — a class, because it has dependencies - Callbacks:
onSomething— the parameter name on the widget that accepts the handler - Native:
setMethodCallHandler,setStreamHandler— explicit in the API name
None of these are enforced by the language. But they're consistent enough that when you see _handleX or XHandler, you know the role without reading the implementation.
The takeaway
A handler is not a mysterious concept. It's a function that responds to something that already happened. It doesn't listen, doesn't route, doesn't define data — it does the work.
The word appears everywhere because the pattern appears everywhere. Any system that separates detection from response — and nearly every well-designed system does — has handlers at the response end.
Next time you see "handler" in documentation, a framework API, or someone else's code, you'll know exactly what role it plays: it's the function that answers the question "OK, that happened — now what?"
Next in the series
The next post goes deeper into the operating system — into the concept we mentioned in the FFI series when we said a library gets "loaded into the process's address space." What is a process? What's inside one? How does your Flutter app's process differ on Android and iOS? And what happens at the boundary between your process and everything else on the device?