Lesson 29 of 81
A server-side render error can fail silently from your monitoring's point of view — React shows the client a graceful fallback, and if nothing is watching the server, nobody ever finds out what actually broke. instrumentation.ts is where you make sure something is watching.
11 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
A Server Component throws during rendering. The user sees a clean fallback UI from a React error boundary. Does onRequestError still fire?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
instrumentation.ts (placed at the project root, or inside src/ if you use one) is a special file Next.js looks for to wire up observability — APM tools, OpenTelemetry, error trackers — at the server level, not the request level. It exports up to two functions:
register() runs exactly once, when a new server instance starts up, and must finish before that server starts handling requests. This is where you initialize a tracing SDK or call something like registerOTel('my-app') — setup that should happen a single time per server process, not per request.onRequestError() runs whenever the Next.js server captures a server-side error — during Server Component rendering, in a Route Handler, a Server Action, or proxy.ts. It receives the error, read-only request info (path, method, headers), and a context object describing exactly where the error happened.The reason this exists as a separate hook, distinct from a React error boundary, is that an error boundary's job is to show the user something reasonable when rendering breaks — it was never designed to also notify you. A Server Component that throws during rendering can be caught by a boundary and replaced with a friendly fallback UI, and from the server's logs, nothing looks obviously wrong. onRequestError is the hook that still fires in that exact situation, giving you the error with full request context even though the user just saw a normal-looking fallback.
// instrumentation.ts
import { type Instrumentation } from 'next'
import { registerOTel } from '@vercel/otel'
export function register() {
registerOTel('praxis')
}
export const onRequestError: Instrumentation.onRequestError = async (
err,
request,
context
) => {
const digest =
typeof err === 'object' && err !== null && 'digest' in err
? String(err.digest)
: undefined
await fetch('https://monitoring.example.com/report', {
method: 'POST',
body: JSON.stringify({
message: err instanceof Error ? err.message : String(err),
digest,
routeType: context.routeType, // 'render' | 'route' | 'action' | 'proxy'
routePath: context.routePath,
path: request.path,
}),
})
}
context.routeType alone tells you whether the failure happened during a Server Component render, inside a Route Handler, a Server Action, or proxy.ts — four very different places to go looking for the bug, all reported through the same hook.
register() runs once per server instance, not once per request. Putting per-request logic in it is a category error — it's for process-level setup, and the server won't start handling requests until it resolves.onRequestError fires even when a React error boundary successfully shows a fallback UI. The two aren't competing mechanisms — the boundary handles what the user sees, onRequestError handles what you, the developer, get told about.error may not be your original thrown error. React can transform it during Server Components rendering; the digest property is the reliable way to recover which error actually occurred.instrumentation.js runs in both the Node.js and Edge runtimes, and process.env.NEXT_RUNTIME is how you branch behavior if your setup code (or your onRequestError reporting logic) needs to differ between them.onRequestError must be awaited. Next.js triggers the hook when it captures the error; unawaited work inside it can get cut off before it completes."A component wrapped in an error boundary shows a fallback fine in production, but you have no idea what's actually throwing or how often." This is a real production-monitoring gap, and it tests whether a candidate knows server-side render errors need their own capture path — an error boundary alone isn't observability, it's just a UX safety net.