Centralized Error Logging & Reporting
onError Hook
onError fires on every uncaught error from a handler — log to Sentry or Datadog here without changing the response shape.
What you'll learn
- Add onError to report errors to Sentry
- Keep the response shape in setErrorHandler
- Avoid throwing inside onError
onError is the observation point for failed requests. It fires after a handler throws (or
rejects) and before the response is sent, but it does not change the response — that is the job
of setErrorHandler.
Report to Sentry
import * as Sentry from '@sentry/node';
app.addHook('onError', async (req, reply, err) => {
Sentry.captureException(err, {
extra: {
reqId: req.id,
url: req.url,
method: req.method,
},
user: req.user ? { id: req.user.id } : undefined,
});
}); req.id ties the Sentry event back to the log line. Add user context if your auth hook
populated req.user.
Skip Validation Errors
Validation errors (err.validation) are usually client mistakes, not bugs. Filter them out so
your dashboards stay clean:
app.addHook('onError', async (req, reply, err) => {
if (err.validation) return; // 400s aren't bugs
if (err.statusCode && err.statusCode < 500) return;
Sentry.captureException(err);
}); Don’t Throw From onError
If onError throws, Fastify falls back to the default error handler and your custom error
shape is lost. Wrap risky work in try/catch:
app.addHook('onError', async (req, reply, err) => {
try {
await reportToDatadog(err, req);
} catch (reportErr) {
req.log.warn({ reportErr }, 'failed to report error');
}
}); Roles, Clearly Divided
onError — observe. setErrorHandler — shape the response. onResponse — collect metrics
after the bytes are sent. Each has a single job; that separation keeps the lifecycle easy to
reason about as the app grows.