Skip to main content

Guides

Hooks

Run awaitable callbacks around every send to observe, rewrite, cancel, and report.


Pass hooks to any provider to run awaitable callbacks around every send. before.send can observe, rewrite, or cancel a message; the rest are best-effort observers: an error they throw is swallowed, so logging and telemetry can never break a send. In a bulk mail(array), hooks run once per message.

Define them once in postboi.config.ts and they apply everywhere, including the zero-config mail():

// postboi.config.ts
import { config } from 'postboi'

export default config({
	hooks: {
		// observe, mutate, or throw to cancel (runs before the request)
		before: {
			send({ message }) {
				if (process.env.NODE_ENV !== 'production') return { ...message, to: 'qa@example.com' }
			}
		},
		// success: analytics / audit
		after: {
			send({ provider, message, duration_ms }) {
				track('email.sent', { provider, to: message.to, duration_ms })
			}
		},
		// any failure: report it
		on: {
			error({ error, message }) {
				Sentry.captureException(error, {
					tags: { provider: error.provider },
					extra: { to: message?.to }
				})
			},
			// each retry: observe provider flakiness
			retry({ provider, attempt, status }) {
				console.warn(`${provider} retry ${attempt} (${status})`)
			}
		}
	}
})
// postboi.config.ts
import { config } from 'postboi'

export default config({
	hooks: {
		// observe, mutate, or throw to cancel (runs before the request)
		before: {
			send({ message }) {
				if (process.env.NODE_ENV !== 'production') return { ...message, to: 'qa@example.com' }
			}
		},
		// success: analytics / audit
		after: {
			send({ provider, message, duration_ms }) {
				track('email.sent', { provider, to: message.to, duration_ms })
			}
		},
		// any failure: report it
		on: {
			error({ error, message }) {
				Sentry.captureException(error, {
					tags: { provider: error.provider },
					extra: { to: message?.to }
				})
			},
			// each retry: observe provider flakiness
			retry({ provider, attempt, status }) {
				console.warn(`${provider} retry ${attempt} (${status})`)
			}
		}
	}
})

The hooks

Hook When it runs Can it change the send?
before.send before the request yes: return a new message, or throw
after.send after a success no (observer only)
on.error on any failure no (observer only)
on.retry before each retry no (observer only)

Cancelling a send

Cancel a send from before.send by throwing SkipSendError (e.g. a suppressed or unsubscribed recipient). It’s a PostboiError with code: "skipped", and it does not trigger on.error:

import Resend from 'postboi/resend'
import { SkipSendError } from 'postboi'

const mail = new Resend({
	api_key: RESEND_API_KEY,
	hooks: {
		before: {
			async send({ message }) {
				if (await isSuppressed(message.to)) throw new SkipSendError(`suppressed: ${message.to}`)
			}
		}
	}
})
import Resend from 'postboi/resend'
import { SkipSendError } from 'postboi'

const mail = new Resend({
	api_key: RESEND_API_KEY,
	hooks: {
		before: {
			async send({ message }) {
				if (await isSuppressed(message.to)) throw new SkipSendError(`suppressed: ${message.to}`)
			}
		}
	}
})