# Cordis EventsService Dispatch Modes Explained: emit, parallel, serial, bail, and waterfall

> Master Cordis EventsService dispatch modes: emit, parallel, serial, bail, and waterfall. Understand how they control event listener execution for synchronous, async, and early-return patterns.

- Repository: [Cordiverse/cordis](https://github.com/cordiverse/cordis)
- Tags: deep-dive
- Published: 2026-09-12

---

**Cordis EventsService provides five distinct dispatch modes—emit, parallel, serial, bail, and waterfall—that control how event listeners are executed, ranging from simple synchronous notification to complex async pipelines and early-return patterns.**

The Cordis framework (maintained at `cordiverse/cordis`) exposes a powerful event system through its **`Context`** API, allowing plugins and core services to communicate via type-safe events. These dispatch modes are defined in [`packages/core/src/events.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/events.ts) and determine the execution strategy, error handling, and return value propagation when multiple listeners are registered for the same event.

## Overview of Cordis Event Dispatch Modes

At the core of Cordis’s event architecture is the `EventsService`, which manages listener registration in `_hooks` and dispatches events through five distinct strategies. Each mode serves specific use cases:

- **`emit`**: Simple synchronous broadcasting
- **`parallel`**: Concurrent asynchronous execution
- **`serial`**: Sequential execution with early exit
- **`bail`**: First-success-wins pattern
- **`waterfall`**: Data transformation pipeline

All modes are accessible as methods on the `Context` prototype—`ctx.emit()`, `ctx.parallel()`, `ctx.serial()`, `ctx.bail()`, and `ctx.waterfall()`—providing a unified interface for plugin developers.

## The Five Dispatch Modes Deep Dive

### Emit Mode

**`emit`** calls every listener synchronously and ignores all return values. This is the simplest dispatch strategy, ideal for fire-and-forget notifications where the order of execution and results do not matter.

```typescript
// Simple notification - listeners run synchronously
ctx.emit('user/online', userId)

```

According to the source code in [`packages/core/src/events.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/events.ts), `emit` iterates through matching hooks immediately without awaiting results or collecting return values.

### Parallel Mode

**`parallel`** executes all listeners concurrently using `Promise.allSettled()` and awaits their completion. If any listener throws, the errors are collected and re-thrown as an `AggregateError`. This mode is optimal for I/O-bound operations like broadcasting messages to multiple subsystems.

```typescript
// Concurrent execution - all cache clearers run at once
await ctx.parallel('cache/clear')

```

The implementation in [`packages/core/src/events.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/events.ts) handles error aggregation, ensuring that one failing listener does not prevent others from completing while still surfacing all errors to the caller.

### Serial Mode

**`serial`** calls listeners one after another and stops as soon as a listener returns a non-bailing value (anything other than `null`, `false`, or `undefined`). The returned value is propagated to the caller, making this ideal for sequential permission checks or validation pipelines.

```typescript
// Sequential pipeline with early exit
const authResult = await ctx.serial('auth/check', credentials)
if (authResult) console.log('Authenticated!')

```

As implemented in [`packages/core/src/events.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/events.ts), this mode uses a standard loop that breaks on the first truthy return, preventing unnecessary execution of downstream listeners.

### Bail Mode

**`bail`** is similar to `serial` but strictly optimized for lookup scenarios. It stops at the first non-bailing return value and returns it directly to the caller. Use this for fast-path lookups where the first successful match should win, such as command alias resolution.

```typescript
// First match wins
const command = ctx.bail('command/find', name)
if (command) command.execute(args)

```

Unlike `serial`, `bail` focuses on returning the actual value rather than just checking for existence, making it semantically clearer for resolver patterns.

### Waterfall Mode

**`waterfall`** passes the result of each listener as the first argument to the next listener in the chain. If no listener returns a value (i.e., all return bailing values), a fallback function is invoked. This creates data-transformation pipelines where each step refines the input, such as configuration merging.

```typescript
// Data transformation pipeline
const finalConfig = ctx.waterfall(
  'config/merge',
  baseConfig,
  (merged) => console.log('Merged config:', merged) // fallback
)

```

The `waterfall` implementation chains listeners by passing the previous return value as the first parameter to the next hook, enabling functional composition patterns.

## Implementation Architecture

The event system relies on three key mechanisms defined in [`packages/core/src/events.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/events.ts):

1. **Resolution (`_resolve`)**: Extracts the optional `thisArg`, resolves the event name, and gathers matching hooks while respecting the `global` flag and any `Context.filter`. It also emits an internal `dispatch` event for diagnostics.

2. **Hook Registration**: `EventsService.on()` stores listeners in `_hooks`, wrapping them with `ctx.reflect.bind()` to correctly inject the current `Context` instance.

3. **Lifecycle Integration**: As defined in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts), listeners are automatically disposed when their owning fiber ends, preventing memory leaks in plugin lifecycles.

The type safety is enforced through the `Events` interface, which provides compile-time checking for event names and argument types across all dispatch modes.

## Summary

- **emit**: Synchronous broadcast, no return value handling
- **parallel**: Concurrent async execution with `AggregateError` collection
- **serial**: Sequential execution stopping at first non-bailing return
- **bail**: First non-bailing return wins (lookup pattern)
- **waterfall**: Result chaining with fallback support

All modes are exposed via `Context` methods in [`packages/core/src/events.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/events.ts) and support automatic cleanup through Cordis’s fiber system ([`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts)).

## Frequently Asked Questions

### What is the difference between serial and bail modes in Cordis?

Both `serial` and `bail` stop execution when a listener returns a non-bailing value, but `bail` is specifically designed for lookup patterns where you want the first successful result returned immediately. `serial` is better suited for validation pipelines where you check if any step succeeds (like authentication), while `bail` fits resolver patterns (like finding a command handler) where you need the actual returned value.

### How does parallel mode handle listener errors?

When using `ctx.parallel()`, Cordis executes all listeners concurrently using `Promise.allSettled()`. If one or more listeners throw, the implementation collects all errors and re-throws them as a single `AggregateError`. This ensures that non-failing listeners complete their work while still surfacing all failures to the caller for proper error handling.

### When should I use waterfall instead of serial?

Use `waterfall` when you need to transform data through a chain of listeners, where each step receives the output of the previous step as its first argument. Use `serial` when you want independent checks that might return early, but do not need to pass transformed data between listeners. `waterfall` is ideal for configuration merging or request preprocessing pipelines.

### How are event listeners automatically cleaned up in Cordis?

Listeners registered via `EventsService.on()` are wrapped using `ctx.reflect.bind()` and integrated with the fiber lifecycle system defined in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts). When the Context's fiber is disposed, the effect system automatically removes all associated hooks from `_hooks`, preventing memory leaks without manual unsubscribe calls.