Aviary
Reference

Configuration

Every NotificationsModule.forRoot and forRootAsync option, the three lifecycle events, the error policy, and the error classes — the full reference for wiring the core.

NotificationsModule.forRoot(options) provides the NotificationService and the delivery engine. Most apps call it with no arguments — the defaults are sensible. Reach for options when you queue notifications or swap the dispatcher.

NotificationsModule.forRoot({
  notifications: [InvoicePaid, PaymentFailed],
  resolveNotifiable: (ref) => users.findOneByOrFail({ id: Number(ref.id) }),
  errorPolicy: 'continueOnError',
  global: true,
  ...bullmqDispatcher(), // from @dudousxd/nestjs-notifications-bullmq — see Dispatchers
});

Options

All options are optional.

notifications

NotificationClass[] — the rehydration registry. List every notification class that may be dispatched asynchronously so a worker can rebuild it from its serialized name. Not needed for sync-only apps. See Async dispatch.

resolveNotifiable

(ref: NotifiableRef) => Promise<Notifiable> | Notifiable — reloads a recipient from its { type, id } reference inside an async worker. Required if any notification sets shouldQueue and is processed out of process. Because it usually needs an injected repository, configure it with forRootAsync.

errorPolicy

'continueOnError' | 'failFast' — what happens when one channel throws. Defaults to 'continueOnError'. See Error handling below.

global

boolean — register the module globally so NotificationService is injectable everywhere without re-importing. Defaults to true.

dispatcher

Type<DispatchDriver> — override the async dispatch driver. Defaults to the SyncDispatcher (inline delivery). Provide the dispatcher's own dependencies through imports / providers. The SyncDispatcher stays registered even when you override this, so custom dispatchers can delegate to inline delivery. See Dispatchers.

dispatchGuards

DispatchGuardOptions — opt-in dedup (idempotency) and throttle (rate-limit) applied before any channel runs. Notifications opt in per-instance via idempotencyKey() / throttle(). Omitting it is a no-op. See Dispatch guards.

localization

LocalizationOptions ({ defaultLocale?, resolver?, translator?, catalog? }) — per-recipient translation of channel payloads. Omitting it is a no-op; payload methods receive a localization argument only when it's configured. See Localization.

imports

ModuleMetadata['imports'] — extra modules the dispatcher or channels need, e.g. BullModule.registerQueue(...) for the BullMQ dispatcher.

providers

Provider[] — extra providers, e.g. the dispatcher's config token.

forRootAsync

When resolveNotifiable (or a dispatcher's config) depends on other providers, use forRootAsync. It mirrors forRoot but resolves the options through a factory:

NotificationsModule.forRootAsync({
  inject: [UserRepository],
  useFactory: (users: UserRepository) => ({
    notifications: [InvoicePaid],
    resolveNotifiable: (ref) => users.findOneByOrFail({ id: Number(ref.id) }),
  }),
});

The async options accept useFactory, inject, imports, providers, dispatcher, and global. errorPolicy still defaults to 'continueOnError' if your factory doesn't set it.

Events

The runner emits three lifecycle events through @nestjs/event-emitter for each channel of each send. The event names live on NotificationEvents:

NameConstantEvent classWhen
notification.sendingNotificationEvents.sendingNotificationSendingEventBefore a channel attempts delivery
notification.sentNotificationEvents.sentNotificationSentEventAfter a channel delivers successfully
notification.failedNotificationEvents.failedNotificationFailedEventWhen a channel throws

Every event carries notifiable, notification, and channel. NotificationFailedEvent adds the thrown error.

Listen with @OnEvent:

notification-metrics.listener.ts
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import {
  NotificationEvents,
  type NotificationSentEvent,
  type NotificationFailedEvent,
} from '@dudousxd/nestjs-notifications-core';

@Injectable()
export class NotificationMetrics {
  @OnEvent(NotificationEvents.sent)
  onSent(event: NotificationSentEvent) {
    metrics.increment('notification.sent', { channel: event.channel });
  }

  @OnEvent(NotificationEvents.failed)
  onFailed(event: NotificationFailedEvent) {
    metrics.increment('notification.failed', { channel: event.channel });
    logger.error(event.error);
  }
}

Because events are per-channel, a notification routed to three channels emits three sending events and three sent/failed events. This is what powers the Telescope watcher.

Send results & hooks

Events are for cross-cutting listeners; for the outcome of a specific call, every send / notify / sendNow / sendAsync returns a SendResult[] — one entry per notifiable, each with a ChannelResult per channel (status, plus the transport response or error). The status is one of:

StatusMeaning
sentDelivered to the channel transport.
failedThe channel threw.
skippedGated out — shouldSend() returned false, or a preference muted the channel.
queuedHanded to an async dispatcher (or deferred for later).
suppressedA dispatch guard deduped it (idempotency).
throttledA dispatch guard rate-limited it (overflow: 'drop').
deferredHeld to be re-delivered later — e.g. quiet hours or a throttle overflow: 'defer'.

A notification can also gate and observe its own delivery: shouldSend(notifiable, channel) skips a channel at delivery time (recorded as skipped), and afterSending(notifiable, channel, response) runs after each successful delivery with the transport response. See Send results for the full detail.

Error handling

The errorPolicy decides what happens when a single channel's send() throws.

continueOnError (default)

Channels are isolated. Every channel runs (in parallel), and a failure in one does not prevent the others. Each failure is logged and surfaced through the notification.failed event — it is not rethrown, so send() resolves regardless. Use this so a flaky SMTP server can't stop the in-app database row from being written.

failFast

Channels run sequentially, and the first failure rethrows out of send() — remaining channels don't run. The notification.failed event still fires for the channel that threw. Use this when a delivery failure should abort the operation that triggered it.

NotificationsModule.forRoot({ errorPolicy: 'failFast' });

Error classes

The core exports these error types:

  • ChannelNotRegisteredError — a notification's via() named a channel no driver was registered for. The message lists the registered channels. Under continueOnError it's emitted as a failed event and logged; under failFast it's rethrown. (Usually means a channel module wasn't imported.)
  • MissingChannelMethodError — a notification was routed to a channel but doesn't implement the channel's to<Channel>() method. Thrown from inside the channel; follows the same policy. See the custom channel recipe.
  • NotificationSerializationError — a queued notification can't be serialized or rehydrated: e.g. a queued notifiable without toNotifiableRef(), or async dispatch configured without resolveNotifiable. Thrown at dispatch time so you find out immediately. See Async dispatch.

See also

  • Async dispatch — the notifications / resolveNotifiable round trip
  • Dispatchers — choosing and configuring a dispatcher
  • Testing — assert sends without delivering

On this page