Fastify
Register the @unhandled/fastify plugin in a Fastify 5 app to get route-named requests, signed-in users, request bodies and server errors in Unhandled.
@unhandled/fastify is Unhandled for Fastify 5. It starts @unhandled/node, which traces the HTTP server, outgoing requests and databases. Its plugin names each server span after the matched route, adds the signed-in user, records request and response bodies when capture is on, and reports server errors. The resource carries unhandled.framework.name=fastify.
The package re-exports everything from @unhandled/node, so captureException, setUser, record and the rest import from @unhandled/fastify too. Its init is the Node SDK's init with the Fastify layer added.
Requirements
- Fastify 5.
- Node.js 24 or later.
- A service DSN. See Services.
Install
npm install @unhandled/fastifyStart the SDK
Load the register module ahead of your entry file:
UNHANDLED_DSN=https://[email protected]/0b4f6c2e-8a1d-4c3e-9f7a-1234567890ab \
node --import @unhandled/fastify/register server.js@unhandled/fastify/register hooks ES module loading and starts the SDK from UNHANDLED_DSN, UNHANDLED_ENV (or APP_ENV) and OTEL_SERVICE_NAME. Prefer it over @unhandled/node/register, which leaves the framework off the resource.
To pass options in an ES module app, write your own register file and load it with --import:
// instrument.ts
import { startWithEsmHook } from '@unhandled/node/hook'
await startWithEsmHook(async () => {
const { init } = await import('@unhandled/fastify')
init({ serviceName: 'shop', http: { captureBodies: { enabled: true } } })
})In CommonJS or a bundled app, call init(options) from @unhandled/fastify before fastify is first required. init takes every Node SDK option.
Register the plugin
import Fastify from 'fastify'
import { unhandled } from '@unhandled/fastify'
const app = Fastify()
await app.register(unhandled, {
user: (request) =>
request.user ? { id: request.user.id, email: request.user.email } : undefined,
})
app.get('/orders/:id', showOrder)Register the plugin before your routes. It is wrapped with fastify-plugin, so its hooks apply to the whole app and not only to its own encapsulation context.
Routes
The plugin's onRequest hook sets http.route from request.routeOptions.url and renames the span, for example GET /orders/:id. Requests that match no route keep the bare method name. Requests groups by that route.
@unhandled/node keeps @opentelemetry/instrumentation-fastify off, because that package is deprecated. The plugin creates no spans of its own, so the server span is the one from the HTTP instrumentation. For a span per hook and handler, add @fastify/otel, which the Fastify team maintains.
Users
The onSend hook calls user(request, reply) after your auth hooks and handler have run. Return { id, email?, name? } or undefined. The result lands on the server span as user.id, user.email and user.name, and on error reports for that request.
Request and response bodies
With http.captureBodies on, onSend records the parsed request body and the serialized response payload. Parsed objects are stored as JSON. Buffers and streams are not recorded. Redaction runs before anything is stored.
Errors
The onError hook reports with mechanism fastify.error_handler and handled: false. Fastify answers a thrown error with its status or statusCode when that is a 4xx or 5xx code, and with 500 otherwise. The plugin reports only the errors answered with a 5xx.
| Error | Reported |
|---|---|
No status or statusCode, so Fastify answers 500 | Yes |
status or statusCode from 500 to 599 | Yes |
Validation errors, @fastify/sensible 4xx errors, any other 4xx code | No. They are client errors. |
| Requests handled by the not-found handler | No. They never reach onError. |
Matched by ignoreExceptions, ignoreStatuses or ignoreCodes | No |
Uncaught exceptions and unhandled rejections outside a request are reported by the process handlers.
Verify the setup
Add a route that throws:
app.get('/debug/boom', async () => { throw new Error('Refund provider is unavailable') })Start the app with the register module and request
/debug/boomonce.In Unhandled, open Traces. The request shows as
GET /debug/boom.Open Exceptions. The error shows with mechanism
fastify.error_handlerand its stack trace.
Exports
| Export | Effect |
|---|---|
init(options?) | Starts the SDK with the Fastify layer. Takes UnhandledFastifyOptions, the Node SDK options. |
unhandled | The plugin. Its options are UnhandledPluginOptions, with user as a UserResolver. |
Everything from @unhandled/node | captureException, setUser, track, record, flush and the rest. See Exports. |
@unhandled/fastify/register | Side effect only. Starts the SDK from the environment. |