Hooks
Concepts

Hooks

Observe lifecycle events without side effects

Hooks observe the send pipeline without affecting it. They fire at specific points in the lifecycle — after validation, after render, after delivery, or on error — and let you log, record metrics, or trigger side effects without changing whether or how a notification is delivered. A failing hook never blocks a send or stops other hooks from running.

If removing a piece of logic would change whether a notification goes out, it belongs in middleware, not a hook.

Lifecycle

Four hooks fire in a fixed order during each send. onError fires on any failure regardless of where it originated.

validate
→
onBeforeSend
→
middleware
→
render
→
onExecute
→
transport
→
onAfterSend
any failure →
onError

Prop

Type

Attaching hooks

Pass hooks to createClient. Each hook accepts a single handler or an array of handlers executed in order:

const mail = createClient({
  catalog,
  transportsByChannel: { email: smtpTransport({ host: '...' }) },
  hooks: {
    onBeforeSend: ({ route, messageId }) => {
      console.log(`sending ${route} [${messageId}]`);
    },
    onAfterSend: ({ route, result, durationMs }) => {
      metrics.recordSend(route, durationMs);
    },
    onError: [
      ({ route, error, phase }) => {
        errorTracker.capture(error, { route, phase });
      },
      ({ route, error }) => {
        console.error(`[${route}] ${error.message}`);
      },
    ],
  },
});

Plugins can also carry hooks. Plugin hooks and client hooks merge — they don't override each other. See Plugins.

Hook context

Each hook receives a context object that builds on the previous one. Later hooks get more information:

Prop

Type

Error isolation

Hook failures are isolated from the send pipeline and from each other:

  • A failing hook does not prevent the notification from being delivered.
  • If one handler in an array throws, the remaining handlers still run.
  • Hook errors are routed to onError with phase: 'hook'. If onError itself throws, the error is logged but never propagated to the caller.

This means you can safely add observability hooks without risking delivery failures.

On this page