Skip to the solution
MediumFrontend7 min read

Next.js instrumentation hook — observability in Next.js 15

169 views

The problem

Question presented to candidate: "Where do you initialise tracing in a Next.js app, and how do you capture server-side errors that never reach an error boundary?"

What a strong answer should cover:

  • The instrumentation file at the project root (or src/) is the designated place. It exports two optional functions.
  • register() runs once when the server starts, before any request. That is where OpenTelemetry, an APM agent, or any global initialisation goes — importantly, before the code it needs to patch is loaded.
  • onRequestError(error, request, context) is called for server-side errors, including the ones no error boundary can see: route handlers, Server Actions, and Server Component renders.
  • The context argument is the useful part: it says which router (App or Pages), the route path, the route type (a render, a route handler, an action, a proxy) and, for renders, the render source.
  • That lets you route errors sensibly — a failing Server Action is a different alert from a failing static render.
  • It complements error boundaries rather than replacing them: boundaries produce the user-facing fallback, instrumentation produces the operator-facing signal.
  • It pairs with the digest a production boundary shows the user — that is the key you match against.
  • It is server-side only; browser errors still need a client-side reporter.

Clarifying questions expected:

  • "Which Next.js version?" — the hook has moved through experimental flags to stable.
  • "Do we need traces, error reporting, or both?" — register versus onRequestError.

Code / implementation expected: Optional. The two exported function signatures are the answer.

  • nextjs
  • observability

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read · 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactThe two exports, with the context fields taken from the shipped types
import { useState } from "react";

// Not runnable as Next.js code in a browser playground — this is an annotated
// reference plus a live explorer for the context object. The type shapes below
// are read from next/dist/server/instrumentation/types.d.ts in next@16.3.4.

const FILE = `// instrumentation.ts  — project root, or src/
import type { Instrumentation } from "next";

export async function register() {
  // Runs ONCE when the server process starts, before any request is handled.
  // That timing is the point: an APM agent patches modules like http and your
  // database driver as they are imported, so it must run before they are.
  if (process.env.NEXT_RUNTIME === "nodejs") {
    // Most tracing SDKs are Node-only, so import conditionally or the edge
    // build breaks.
    await import("./instrumentation.node");
  }
}

export const onRequestError: Instrumentation.onRequestError = async (
  error,      // unknown
  request,    // { path, method, headers }
  context,    // the useful one — see the table below
) => {
  await sendToMonitoring({
    message: String(error),
    path: request.path,
    method: request.method,
    router: context.routerKind,
    route: context.routePath,
    type: context.routeType,
    source: context.renderSource,
    revalidate: context.revalidateReason,
  });
};`;

// Straight from the installed package's type definitions.
const CONTEXT_FIELDS = [
  ["routerKind", "'Pages Router' | 'App Router'", "which router produced it"],
  ["routePath", "string", "the route that failed"],
  ["routeType", "'render' | 'route' | 'action' | 'proxy'", "page render, route handler, Server Action, or proxy"],
  ["renderSource", "'react-server-components' | 'react-server-components-payload' | 'server-rendering'", "only present for renders"],
  ["revalidateReason", "'on-demand' | 'stale' | undefined", "why a revalidation was running"],
];

const SCENARIOS = [
  { label: "a Server Component threw", routeType: "render", renderSource: "react-server-components", caught: true },
  { label: "a route handler threw", routeType: "route", renderSource: "—", caught: false },
  { label: "a Server Action failed", routeType: "action", renderSource: "—", caught: false },
  { label: "a client render error", routeType: "(not server-side)", renderSource: "—", caught: true },
];

export default function App() {
  const [i, setI] = useState(0);
  const s = SCENARIOS[i];

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 640 }}>
      <pre style={{ background: "#f6f6f8", border: "1px solid #ddd", borderRadius: 8,
                    padding: 12, fontSize: 12, overflowX: "auto", whiteSpace: "pre-wrap" }}>
{FILE}
      </pre>

      <h4 style={{ margin: "14px 0 6px" }}>The context argument</h4>
      <table style={{ width: "100%", fontSize: 12, borderCollapse: "collapse" }}>
        <tbody>
          {CONTEXT_FIELDS.map(([name, type, note]) => (
            <tr key={name} style={{ borderBottom: "1px solid #eee" }}>
              <td style={{ padding: "3px 8px 3px 0", whiteSpace: "nowrap" }}><code>{name}</code></td>
              <td style={{ padding: "3px 8px 3px 0", color: "#4f46e5" }}><code>{type}</code></td>
              <td style={{ color: "#666" }}>{note}</td>
            </tr>
          ))}
        </tbody>
      </table>

      <h4 style={{ margin: "14px 0 6px" }}>Who sees what</h4>
      <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 8 }}>
        {SCENARIOS.map((sc, n) => (
          <button key={sc.label} onClick={() => setI(n)} style={{ fontWeight: n === i ? "bold" : "normal", fontSize: 12 }}>
            {sc.label}
          </button>
        ))}
      </div>
      <div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, fontSize: 13 }}>
        <div>error boundary shows a fallback: <strong style={{ color: s.caught ? "#161" : "#a33" }}>{s.caught ? "yes" : "no"}</strong></div>
        <div>
          onRequestError fires:{" "}
          <strong style={{ color: s.routeType === "(not server-side)" ? "#a33" : "#161" }}>
            {s.routeType === "(not server-side)" ? "no — server-side only" : "yes"}
          </strong>
        </div>
        <div style={{ color: "#666" }}>
          context.routeType = <code>{s.routeType}</code> · renderSource = <code>{s.renderSource}</code>
        </div>
      </div>

      <p style={{ fontSize: 13, color: "#666" }}>
        The two middle scenarios are the reason this hook exists: a route
        handler and a Server Action fail outside any render, so no boundary is
        in the window and the user-facing mechanism reports nothing at all.
      </p>
    </div>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 67 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextMedium

What is `React.memo` and when would you use it?

Continue