Files
2026-05-27 23:29:53 +01:00

3.7 KiB

error-codes — Notification-channel error registry

(no submodules)

Purpose

error-codes.ts is the single source of truth for every error the @suaveplan/notification-channel package can throw. It registers a stable namespace NOTIFICATION_CHANNEL with eleven reason codes via defineCodes("NOTIFICATION_CHANNEL", […]) from @suaveplan/error, and exposes a typed SuaveplanError subclass for each reason. The registry preserves the literal-typed union through the public surface so downstream consumers — loggers, problem+json responders, retry wrappers — can instanceof-narrow on a specific subclass or match on the .code discriminant without unsafe casts. The module is side-effect-imported from src/index.ts so the codes are part of the package's public ABI even for consumers that pull a subpath export.

Features

  • Eleven stable reason codes: NOT_FOUND, CHANNEL_DISABLED, TEMPLATE_NOT_FOUND, RATE_LIMIT_EXCEEDED, ROUTING_FAILED, DELIVERY_FAILED, PROVIDER_ERROR, RATE_LIMITED, INVALID_RECIPIENT, CHANNEL_UNAVAILABLE, TIMEOUT.
  • One typed subclass per code extending an abstract NotificationChannelError base.
  • Compile-time-frozen NOTIFICATION_CHANNEL_CODES record whose values are literal "NOTIFICATION_CHANNEL.REASON" strings.
  • Every constructor accepts the standard SuaveplanErrorOptions bag (cause, context, traceId, clock).
  • RateLimitedError carries an additional retryAfterMs field merged into context.

Usage: Basic

import {
    NOTIFICATION_CHANNEL_CODES,
    ChannelDisabledError,
    type NotificationChannelErrorCode,
} from "@suaveplan/notification-channel/error-codes";

try {
    /* ... call the router ... */
} catch (err) {
    if (err instanceof ChannelDisabledError) {
        const code: NotificationChannelErrorCode = err.code;
        void code; // "NOTIFICATION_CHANNEL.CHANNEL_DISABLED"
    }
}

void NOTIFICATION_CHANNEL_CODES.RATE_LIMITED;

Usage: Advanced

import { RateLimitedError } from "@suaveplan/notification-channel/error-codes";

throw new RateLimitedError({
    message: "notification.errors.rateLimited",
    retryAfterMs: 30_000,
    context: { provider: "twilio" },
    traceId: "trace-abc",
});

API Reference

  • NOTIFICATION_CHANNEL_CODES — frozen record { readonly [K in Reason]: "NOTIFICATION_CHANNEL.${K}" }.
  • NotificationChannelErrorCode — discriminated union of all eleven literal codes.
  • NotificationChannelError — abstract base extending SuaveplanError<NotificationChannelErrorCode>.
  • Concrete subclasses: NotificationNotFoundError, ChannelDisabledError, TemplateNotFoundError, RateLimitExceededError, RoutingFailedError, DeliveryFailedError, ProviderErrorError, RateLimitedError, InvalidRecipientError, ChannelUnavailableError, DeliveryTimeoutError.

Implementation Notes

The registry uses defineCodes from @suaveplan/error rather than a runtime ErrorCodes registry. defineCodes is purely type-level plus a frozen object, so it has no module-load side effect beyond the freeze, and the literal-typed union survives tree-shaking. pickOpts discards undefined fields explicitly to remain compatible with exactOptionalPropertyTypes: true in tsconfig.base.json. The split between "local lifecycle errors" and "upstream-provider errors" upstream had two parallel registries (error-codes.ts and errors/notification-errors.ts) — those are unified here so consumers see a single canonical code list. RateLimitedError is the one place the constructor accepts a typed sidecar (retryAfterMs) — it merges into context so downstream HTTP Retry-After mapping is straightforward.