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:

kotlin
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.

kotlin
// 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:

kotlin
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:

kotlin
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:

kotlin
bus.hasListeners<UserCreated>()   // true/false
bus.listenerCount<UserCreated>()  // number of registered listeners

The 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:

kotlin
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.