NextTech Insights
Next.js error digest in production: find the real server error with onRequestError (2026)
nextjsoperationsloggingobservability

Next.js error digest in production: find the real server error with onRequestError (2026)

DigitalCraft
10 min read

Your production Next.js page shows "An error occurred in the Server Components render" and a digest number. Here is how to trace the digest to the original server error with instrumentation.ts and onRequestError, based on a tested Next.js 15 build.

Table of Contents

How do you find the real error behind a Next.js production digest?

Short answer

In production, Next.js replaces the message of any error thrown in a Server Component with a generic sentence and a numeric digest. The original message and stack stay on the server. To connect the two, export onRequestError from instrumentation.ts, log the digest together with the route and request path, and search your server logs for the number the user saw.

Three details from our test build decide how you should use it:

  • The same error thrown twice produced the same digest, so a digest identifies an error signature, not a single request.
  • Changing one value inside the message produced a different digest.
  • A Route Handler error reached onRequestError without any digest, and its 500 response had an empty body.

The symptom

A page that works locally fails in production, and the error boundary (error.tsx) renders this message instead of the one you threw:

An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.

Next to it, error.digest contains a number such as 4174211443. Reloading shows the same message, and the browser console gives you nothing more.

This is intended behavior. The error.js reference states that errors forwarded from Server Components show a generic message with an identifier to avoid leaking sensitive details, and that error.digest can be used to match server-side logs. Errors thrown in Client Components keep their original message.

What we tested

We built a minimal App Router app with Next.js 15.1.9, ran next build and next start, and sent six requests:

  • /boom?k=a: a Server Component throws orders query timed out ... (k=a). We requested it twice.
  • /boom?k=b: the same code path, where only the k value in the message differs.
  • /api/boom: a Route Handler GET throws.
  • /act: a Server Action throws payment provider returned 502.
  • /missing: the page calls notFound().

The instrumentation.ts file at the project root logged one JSON line per error:

// instrumentation.ts
import type { Instrumentation } from 'next';

export const onRequestError: Instrumentation.onRequestError = async (err, request, context) => {
  const e = err as { message?: string; digest?: string; name?: string };
  console.log(
    JSON.stringify({
      level: 'error',
      source: 'onRequestError',
      digest: e?.digest,
      name: e?.name,
      message: e?.message ?? String(err),
      method: request.method,
      path: request.path,
      routeType: context.routeType,
      routePath: context.routePath,
      renderSource: context.renderSource,
    })
  );
};

Server output

Abridged next start output (stack traces shortened):

{"level":"error","source":"onRequestError","digest":"4174211443","name":"Error","message":"orders query timed out after 5000ms (k=a)","method":"GET","path":"/boom?k=a","routeType":"render","routePath":"/boom","renderSource":"react-server-components"}
 ⨯ Error: orders query timed out after 5000ms (k=a) { digest: '4174211443' }
{"level":"error","source":"onRequestError","digest":"4174211443", ... "path":"/boom?k=a" ...}
{"level":"error","source":"onRequestError","digest":"2392779123", ... "message":"orders query timed out after 5000ms (k=b)" ...}
{"level":"error","source":"onRequestError","name":"Error","message":"webhook signature secret missing","method":"GET","path":"/api/boom","routeType":"route","routePath":"/api/boom"}
{"level":"error","source":"onRequestError","digest":"3242588772","name":"Error","message":"payment provider returned 502","method":"POST","path":"/act","routeType":"action","routePath":"/act","renderSource":"react-server-components-payload"}

What the client received

  • Server Component (/boom): HTTP 500. The error boundary showed the generic message and digest: 4174211443. The original message was not in the HTML.
  • Server Action (/act): HTTP 500. The RSC payload contained only E{"digest":"3242588772"}.
  • Route Handler (/api/boom): HTTP 500 with an empty body and no digest.
  • notFound() (/missing): HTTP 404 with the not-found page. onRequestError was not called.

We also searched the page HTML for the original text. It did not contain orders query, which confirms that the message stays on the server.

Why the digest behaves this way

In the Next.js 15.1.9 source (next/dist/server/app-render/create-error-handler.js), an error without a digest receives one computed as a string hash of err.message + err.stack. That explains the test results:

  • Repeated failures share a digest. The same message and stack produce the same hash. Treat the digest as a grouping key, and do not assume it points to one request.
  • Dynamic values split the group. Putting an ID, a timestamp, or a query value in the message creates a new digest for each value. Keep variable data in structured fields instead of the message if you want errors to group.
  • Digests do not survive redeploys reliably. The stack contains positions in the compiled bundle, so a new build can produce a different digest for the same bug. We rebuilt the test app after editing only instrumentation.ts and error.tsx. The unchanged /boom?k=a error moved from page.js:1:4110 to page.js:1:4204, and its digest changed from 4174211443 to 604939349. Record the deployment ID or commit SHA next to the digest.
  • Framework control flow is filtered out. notFound(), redirect(), and dynamic-rendering bailouts carry their own internal digests and are not reported as request errors, which matched our /missing result.

This hashing is an implementation detail rather than a documented contract. Check it again after a major Next.js upgrade.

The fix: a production-ready onRequestError

The instrumentation.js reference documents onRequestError(error, request, context) as the hook for reporting server errors to an observability provider. It was introduced in Next.js 15.0.0. The file lives in the project root, or inside src/ if you use that folder. Two notes from the reference matter here: await any async work inside the hook, and remember that the error object may have been processed by React, so rely on digest rather than object identity.

A version you can ship:

// instrumentation.ts
import type { Instrumentation } from 'next';

const SAFE_HEADERS = ['user-agent', 'x-request-id', 'x-vercel-id', 'referer'];

export const onRequestError: Instrumentation.onRequestError = async (err, request, context) => {
  const error = err instanceof Error ? err : new Error(String(err));
  const digest =
    typeof err === 'object' && err !== null && 'digest' in err ? String(err.digest) : undefined;

  const headers: Record<string, string> = {};
  for (const name of SAFE_HEADERS) {
    const value = request.headers[name];
    if (value) headers[name] = Array.isArray(value) ? value.join(',') : value;
  }

  const event = {
    level: 'error',
    digest,
    name: error.name,
    message: error.message,
    stack: error.stack,
    method: request.method,
    path: request.path.split('?')[0],
    headers,
    routerKind: context.routerKind,
    routePath: context.routePath,
    routeType: context.routeType,
    renderSource: context.renderSource,
    revalidateReason: context.revalidateReason,
    deployment: process.env.VERCEL_DEPLOYMENT_ID ?? process.env.VERCEL_GIT_COMMIT_SHA,
    runtime: process.env.NEXT_RUNTIME,
  };

  console.error(JSON.stringify(event));

  // Optional: forward to your provider. Await it so the platform does not
  // freeze the function before the request completes.
  // await fetch('https://example.com/errors', { method: 'POST', body: JSON.stringify(event) });
};

Design choices worth keeping:

  • Allowlist headers. request.headers includes cookie and authorization. Do not send the whole object to a third-party service.
  • Drop the query string. Search parameters often carry emails, tokens, or IDs. Log the path, then add specific parameters only when you need them.
  • Tag the deployment. Because the digest can change between builds, digest + deployment is the lookup key that actually works during an incident. On Vercel, VERCEL_DEPLOYMENT_ID and VERCEL_GIT_COMMIT_SHA are system environment variables available at runtime when system environment variables are enabled for the project. When self-hosting, inject your own commit SHA at build time.
  • Keep it cheap and non-throwing. The hook runs on the failing request. A slow or failing reporter should not add a second outage. Put a timeout on any network call.

We ran this version against the second test build with Cookie, Authorization, and an email query parameter on the request. The logged event kept user-agent, path: "/boom", routerKind, and the injected commit SHA, and none of the three sensitive values appeared in the server output.

If you use both runtimes, branch on process.env.NEXT_RUNTIME ('nodejs' or 'edge') as the reference shows, and keep Node-only SDKs out of the Edge path.

Cover the cases the digest does not

Route Handlers: return your own ID

Our Route Handler failure produced no digest and an empty 500 body, so an API client has nothing to report. Catch errors at the handler boundary and return a correlation ID that you also log:

// app/api/orders/route.ts
export async function GET() {
  const errorId = crypto.randomUUID();
  try {
    return Response.json(await loadOrders());
  } catch (err) {
    console.error(JSON.stringify({ level: 'error', errorId, message: String(err) }));
    return Response.json({ error: 'Internal error', errorId }, { status: 500 });
  }
}

Once you catch the error yourself, onRequestError no longer sees it, so log it at this point.

Client-side errors

onRequestError only runs on the server. Errors thrown in the browser reach error.tsx with their real message and no server log entry. Report them from the error boundary, and include the digest when it exists so both sides join in one search:

'use client';

import { useEffect } from 'react';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // Replace with your provider's client SDK.
    console.error({ digest: error.digest, message: error.message });
  }, [error]);

  return (
    <div>
      <p>Something went wrong.</p>
      {error.digest && <p>Reference: {error.digest}</p>}
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

Showing the digest as a reference number gives support staff a value they can paste into a log search. In Next.js 16.3 and later, the error boundary also receives a stable retry() prop that re-fetches the segment. Use reset() on Next.js 15.

Root layout errors

error.tsx does not wrap the layout in the same segment. Add app/global-error.tsx so root-layout failures still show a reference instead of the default page.

Check log retention before you need it

A digest is only useful while the matching log line still exists. Vercel's runtime logs documentation lists retention of 1 hour on Hobby and 1 day on Pro (30 days with Observability Plus). A user who reports a digest the next morning may point at a log line that is already gone.

If reports arrive later than your retention window, forward onRequestError events to a log drain or an error tracker. The digest then becomes a stable search key in a system you control.

Diagnostic flow during an incident

  1. Get the digest from the user, a screenshot, or the error boundary UI.
  2. Identify the deployment the user hit. If a redeploy happened since, search that deployment's logs first.
  3. Search runtime logs for the digest. On Vercel, runtime logs capture console output from functions, and free-text search covers the log message, so a digest inside your JSON line is searchable. When self-hosting, search the process output or your log shipper.
  4. Read routeType: render means a page or layout, action means a Server Action, route means a Route Handler, and proxy means Proxy (Middleware in older versions).
  5. If the digest is not in the logs, check whether instrumentation.ts is in the right folder (root or src/), whether the error was caught and swallowed in your code, and whether it was really a client-side error.
  6. After the fix, reproduce the failing request on the new deployment and confirm that the same message no longer appears for that routePath. Do not compare digests across the two builds, because they can differ even for the same bug.

Verify your setup in five minutes

Add a temporary route that throws, build in production mode, and compare both sides:

npm run build && npm run start
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/debug-throw

Pass criteria:

  • The browser shows the generic message and a digest, not your message.
  • The server prints one onRequestError line with the same digest, the original message, routeType: "render", and no cookies.
  • Removing the test route before deploying leaves no public way to trigger errors.

Sources

Disclaimer

General engineering guidance based on the documentation and a local test at the versions listed above. Digest generation is internal to Next.js and may change between releases. Verify the behavior on the version and hosting platform you run.

Popular

  1. 1Permit2 explained (Web3): why approvals changed and how to use it safely (checklist)
  2. 2Read wallet signing screens (Web3): a 30-second checklist to avoid permission traps
  3. 3Spec-to-implementation prompt template (AI development): how to stop the model from guessing
  4. 4Revoke token approvals on EVM: how to audit allowances safely (checklist)
  5. 5Clarifying questions checklist (AI development): what to ask before you let an LLM build

Related Articles