docs

Hono

Add the @unhandled/hono middleware to a Hono app on Node.js to get route-named requests, signed-in users, request bodies and server errors in Unhandled.

@unhandled/hono is Unhandled for Hono on Node.js with @hono/node-server. It starts @unhandled/node, whose HTTP instrumentation creates a server span for every request the Node server receives. The middleware names that 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=hono.

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

Requirements

  • Hono 4.8 or later, served by @hono/node-server.
  • Node.js 24 or later.
  • A service DSN. See Services.

Hono on Cloudflare Workers, Deno or Bun is not covered. The package needs the server span that the Node HTTP instrumentation creates.

Install

Terminal
npm install @unhandled/hono

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/hono/register server.js

@unhandled/hono/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/hono')
  init({ serviceName: 'shop', http: { captureBodies: { enabled: true } } })
})

In CommonJS or a bundled app, call init(options) from @unhandled/hono before anything else loads. init takes every Node SDK option.

Add the middleware

ts
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { unhandled } from '@unhandled/hono'

const app = new Hono<{ Variables: { user?: { id: string } } }>()
app.use(unhandled({ user: (c) => c.get('user') }))

app.get('/orders/:id', (c) => c.json(loadOrder(c.req.param('id'))))

serve({ fetch: app.fetch, port: 3000 })

Register unhandled(options?) first, so every other middleware and handler runs inside it. The middleware does its work after the rest of the chain has run.

Routes

The middleware sets http.route to the path of the route that answered, read with hono/route, and renames the span, for example GET /orders/:id. When a use() or all() entry answers with a 404, no route matched and the span keeps the bare method name. Requests groups by that route.

Users

user(c) runs after your auth middleware, so values set with c.set() are there. 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, the middleware records request and response bodies whose content type is JSON or a URL-encoded form. Redaction runs before anything is stored.

The middleware never reads a request body on its own. It records the body your handler already read with c.req.json() or c.req.text(), and records nothing when the handler did not read it. For the response it reads a copy of a body that is already in memory, such as one from c.json(). A streamed response is not waited for and its body is not recorded, so the client gets every response as soon as the handler returns it.

Errors

The middleware reports c.error with mechanism hono.on_error and handled: false. Hono sets c.error before it calls your app.onError handler, so a custom handler changes the response but not the report.

ErrorReported
Any error that is not an HTTPException. Hono answers it with 500.Yes
HTTPException with a status of 500 or moreYes
HTTPException with a 4xx statusNo. It is a response you chose to send.
Matched by ignoreExceptions, ignoreStatuses or ignoreCodesNo

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 hono.on_error and its stack trace.

Exports

ExportEffect
init(options?)Starts the SDK with the Hono layer. Takes UnhandledHonoOptions, the Node SDK options.
unhandled(options?)The middleware. options.user is a UserResolver.
Everything from @unhandled/nodecaptureException, setUser, track, record, flush and the rest. See Exports.
@unhandled/hono/registerSide effect only. Starts the SDK from the environment.