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);
}