3 of 5
Middleware & Errors
Intercept dispatch, handle errors gracefully, inspect the bus, and leverage event hierarchy.
Middleware
Middleware wraps the dispatch pipeline. Each middleware receives the event and a next function. Call next(event) to continue to the next middleware or the actual listener dispatch. Omit the call to short-circuit:
bus.use { event, next ->
val start = System.nanoTime()
next(event)
println("${event::class.simpleName} in ${System.nanoTime() - start}ns")
}You can register multiple middleware — they form a chain and execute in registration order. Middleware runs even when individual listeners throw errors.
// Logging middleware
bus.use { event, next ->
println("Before: ${event::class.simpleName}")
next(event)
println("After: ${event::class.simpleName}")
}
// Auth gate — short-circuits if not authenticated
bus.use { event, next ->
if (isAuthenticated()) next(event)
}Middleware is cleared when you call bus.clear().
Error resilience
When a listener throws, remaining listeners still execute. Errors are collected and rethrown after all listeners have run. A single error is thrown directly; multiple errors are wrapped in CompositeEventException:
try {
bus.emit(UserCreated("Alice"))
} catch (e: CompositeEventException) {
e.errors.forEach { println(it.message) }
}Provide a custom error handler to change this behavior — for example, to log instead of throw:
val bus = EventBus(container, onError = { e ->
logger.error("Dispatch failed", e)
})In the coroutines module, onError is a suspend function.
Inspector
Query the event bus state at runtime:
bus.hasListeners<UserCreated>() // true/false
bus.listenerCount<UserCreated>() // number of registered listenersThe Inspector interface counts both class-based and lambda listeners, including catch-all handlers.
Event hierarchy
Listeners registered for a parent event type are also invoked when a subtype is emitted. This lets you define broad handlers alongside specific ones:
interface DomainEvent : Event
data class UserCreated(val name: String) : DomainEvent
data class OrderPlaced(val id: String) : DomainEvent
// Receives UserCreated, OrderPlaced, and any future DomainEvent
bus.on<DomainEvent> { event -> println("Domain: $event") }
// Receives only UserCreated
bus.on<UserCreated> { event -> println("User: ${event.name}") }
bus.emit(UserCreated("Alice"))
// prints: "Domain: UserCreated(name=Alice)"
// prints: "User: Alice"The onAny catch-all works the same way — it's registered on the Event root type, so it receives everything.
Next steps
If your listeners need to call suspend functions, see the coroutines module.