Next.js error digest in production: find the real server error with onRequestError (2026)
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
- The symptom
- What we tested
- Server output
- What the client received
- Why the digest behaves this way
- The fix: a production-ready onRequestError
- Cover the cases the digest does not
- Route Handlers: return your own ID
- Client-side errors
- Root layout errors
- Check log retention before you need it
- Diagnostic flow during an incident
- Verify your setup in five minutes
- Sources
- Internal links
- Disclaimer
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
onRequestErrorwithout 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 throwsorders query timed out ... (k=a). We requested it twice./boom?k=b: the same code path, where only thekvalue in the message differs./api/boom: a Route HandlerGETthrows./act: a Server Action throwspayment provider returned 502./missing: the page callsnotFound().
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 anddigest: 4174211443. The original message was not in the HTML. - Server Action (
/act): HTTP 500. The RSC payload contained onlyE{"digest":"3242588772"}. - Route Handler (
/api/boom): HTTP 500 with an empty body and no digest. notFound()(/missing): HTTP 404 with the not-found page.onRequestErrorwas 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.tsanderror.tsx. The unchanged/boom?k=aerror moved frompage.js:1:4110topage.js:1:4204, and its digest changed from4174211443to604939349. 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/missingresult.
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.headersincludescookieandauthorization. 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 + deploymentis the lookup key that actually works during an incident. On Vercel,VERCEL_DEPLOYMENT_IDandVERCEL_GIT_COMMIT_SHAare 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
- Get the digest from the user, a screenshot, or the error boundary UI.
- Identify the deployment the user hit. If a redeploy happened since, search that deployment's logs first.
- Search runtime logs for the digest. On Vercel, runtime logs capture
consoleoutput 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. - Read
routeType:rendermeans a page or layout,actionmeans a Server Action,routemeans a Route Handler, andproxymeans Proxy (Middleware in older versions). - If the digest is not in the logs, check whether
instrumentation.tsis in the right folder (root orsrc/), whether the error was caught and swallowed in your code, and whether it was really a client-side error. - 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
onRequestErrorline 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
- instrumentation.js file convention (Next.js Docs)
- error.js file convention (Next.js Docs)
- Instrumentation guide (Next.js Docs)
- Runtime logs (Vercel Docs)
- System environment variables (Vercel Docs)
- Test environment: Next.js 15.1.9, React 19.0.0, Node.js 22.16.0, TypeScript strict mode,
next build+next starton Windows, run on 2026-09-29.
Internal links
- Parent hub: Next.js security
- Related:
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
- 1Permit2 explained (Web3): why approvals changed and how to use it safely (checklist)
- 2Read wallet signing screens (Web3): a 30-second checklist to avoid permission traps
- 3Spec-to-implementation prompt template (AI development): how to stop the model from guessing
- 4Revoke token approvals on EVM: how to audit allowances safely (checklist)
- 5Clarifying questions checklist (AI development): what to ask before you let an LLM build