Error Handling
Reference

Error Handling

Handle and recover from notification failures

Every error thrown by Better-Notify is a NotifyRpcError (or a subclass). Each error carries a machine-readable code so you can branch without string matching on message, plus optional route and messageId for context.

import { NotifyRpcError } from '@betternotify/core';

const [err, result] = await handlePromise(
  mail.welcome.send({ to: 'ada@example.com', input: { name: 'Ada' } }),
);

if (err instanceof NotifyRpcError) {
  console.log(err.code);      // 'VALIDATION', 'PROVIDER', 'RENDER', etc.
  console.log(err.route);     // 'transactional.welcome'
  console.log(err.messageId); // UUID for this send attempt
}

Error codes

Prop

Type

Handling specific errors

Validation errors

NotifyRpcValidationError carries structured issues from your schema validator:

import { NotifyRpcValidationError } from '@betternotify/core';

const [err] = await handlePromise(
  mail.welcome.send({
    to: 'ada@example.com',
    input: { name: '', verifyUrl: 'not-a-url' },
  }),
);

if (err instanceof NotifyRpcValidationError) {
  console.log(err.code);   // 'VALIDATION'
  console.log(err.route);  // 'transactional.welcome'

  for (const issue of err.issues) {
    console.log(issue.path, issue.message);
    // ['name'] 'String must contain at least 1 character(s)'
    // ['verifyUrl'] 'Invalid url'
  }
}

Rate limit errors

NotifyRpcRateLimitedError carries retryAfterMs for precise retry scheduling:

import { NotifyRpcRateLimitedError } from '@betternotify/core';

const [err] = await handlePromise(
  mail.welcome.send({
    to: 'ada@example.com',
    input: { name: 'Ada', verifyUrl: 'https://example.com/verify' },
  }),
);

if (err instanceof NotifyRpcRateLimitedError) {
  console.log(err.code);         // 'RATE_LIMITED'
  console.log(err.key);          // the rate-limit key that was exceeded
  console.log(err.retryAfterMs); // ms until the window resets

  await new Promise((r) => setTimeout(r, err.retryAfterMs));
  await mail.welcome.send({
    to: 'ada@example.com',
    input: { name: 'Ada', verifyUrl: 'https://example.com/verify' },
  });
}

Error phases

Errors originate from different stages of the pipeline. The onError hook receives a phase field that tells you where:

Prop

Type

Global error handling with onError

Use the onError hook on createClient to catch all errors in one place:

const mail = createClient({
  catalog,
  transportsByChannel: { email: transport },
  hooks: {
    onError: ({ route, error, phase, messageId }) => {
      console.error(`[${phase}] ${route} failed:`, error.message);

      errorTracker.capture(error, {
        route,
        phase,
        messageId,
        code: error.code,
      });
    },
  },
});

Hook failures are isolated — a broken onError handler never prevents other error handlers from running and never blocks delivery. See Hooks for the full isolation model.

JSON serialization

All errors are JSON-serializable via error.toJSON(). This is used internally for queue persistence and structured logging:

const [err] = await handlePromise(
  mail.welcome.send({
    to: 'ada@example.com',
    input: { name: 'Ada', verifyUrl: 'https://example.com/verify' },
  }),
);

if (err instanceof NotifyRpcError) {
  const serialized = err.toJSON();
  // {
  //   name: 'NotifyRpcError',
  //   message: 'Transport send failed for route "welcome": connection refused',
  //   code: 'PROVIDER',
  //   route: 'welcome',
  //   messageId: '550e8400-e29b-41d4-a716-446655440000'
  // }

  await deadLetterQueue.add(serialized);
}

On this page