docs

Express

Install @unhandled/express in an Express 5 app to get route-named requests, signed-in users, request bodies and server errors in Unhandled.

@unhandled/express is Unhandled for Express 5. It starts @unhandled/node, which traces the HTTP server, Express middleware, outgoing requests and databases. On top of that it adds the signed-in user to each request, records request and response bodies when capture is on, and reports server errors from Express's error pipeline. The resource carries unhandled.framework.name=express.

The package re-exports everything from @unhandled/node, so captureException, setUser, record and the rest import from @unhandled/express too. Its init is the Node SDK's init with the Express layer added.

Requirements

  • Express 5.
  • Node.js 24 or later.
  • A service DSN. See Services.

Install

Terminal
npm install @unhandled/express

Start the SDK

Load the register module ahead of your entry file:

Terminal
UNHANDLED_DSN=https://[email protected]/0b4f6c2e-8a1d-4c3e-9f7a-1234567890ab \
  node --import @unhandled/express/register server.js

@unhandled/express/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:

ts
// instrument.ts
import { startWithEsmHook } from '@unhandled/node/hook'

await startWithEsmHook(async () => {
  const { init } = await import('@unhandled/express')
  init({ serviceName: 'shop', http: { captureBodies: { enabled: true } } })
})

In CommonJS or a bundled app, call init(options) from @unhandled/express before express is first required. init takes every Node SDK option.

Add the middleware

ts
import express from 'express'
import { unhandledErrorHandler, unhandledMiddleware } from '@unhandled/express'

const app = express()
app.use(express.json())
app.use(
  unhandledMiddleware({
    user: (req) => (req.user ? { id: req.user.id, email: req.user.email } : undefined),
  })
)

app.get('/orders/:id', showOrder)

app.use(unhandledErrorHandler())
app.use(yourErrorHandler)

Put unhandledMiddleware(options?) before your routes. Put unhandledErrorHandler() after your routes and before your own error handlers.

Routes

The Express instrumentation in @unhandled/node names each server span after the matched route, such as GET /orders/:id, mount paths included, and sets http.route. Requests groups by that route.

If you turn the Express instrumentation off with instrumentations: { '@opentelemetry/instrumentation-express': { enabled: false } }, the middleware sets the route from req.baseUrl and req.route.path instead. In that mode, an error thrown inside a mounted router loses the mount path, because Express resets req.baseUrl once the error leaves the router.

Users

user(req, res) runs when the response finishes, so it sees what your auth middleware put on the request. Return { id, email?, name? } or undefined. The result lands on the request's server span as user.id, user.email and user.name, and on error reports for that request. You can also call setUser() from a handler. See Users.

Request and response bodies

With http.captureBodies on, the middleware records the body your body parser produced and the value passed to res.json() or the string passed to res.send(). Objects are stored as JSON without being serialized and parsed again. Buffers from express.raw() and streamed responses are not recorded. Redaction runs before anything is stored.

Errors

unhandledErrorHandler() reports the error with mechanism express.error_handler and handled: false, then calls next(err). Your error handlers and Express's default response still run.

ErrorReported
No status or statusCode, so Express answers 500Yes
status or statusCode from 500 to 599Yes
status or statusCode from 400 to 499No. It is a client error.
Matched by ignoreExceptions, ignoreStatuses or ignoreCodesNo
Caught in a handler and turned into a responseNo. Call captureException().

Uncaught exceptions and unhandled rejections outside a request are reported by the process handlers.

Verify the setup

  1. Add a route that throws:

    ts
    app.get('/debug/boom', () => {
      throw new Error('Refund provider is unavailable')
    })
  2. Start the app with the register module and request /debug/boom once.

  3. In Unhandled, open Traces. The request shows as GET /debug/boom.

  4. Open Exceptions. The error shows with mechanism express.error_handler and its stack trace.

Exports

ExportEffect
init(options?)Starts the SDK with the Express layer. Takes UnhandledExpressOptions, the Node SDK options.
unhandledMiddleware(options?)Request middleware. options.user is a UserResolver.
unhandledErrorHandler()Error middleware that reports 5xx errors.
Everything from @unhandled/nodecaptureException, setUser, track, record, flush and the rest. See Exports.
@unhandled/express/registerSide effect only. Starts the SDK from the environment.