Next.js error digest (tested)

In a production Next.js App Router build, an uncaught error in a Server Component, Route Handler, or Server Action never reaches the browser. The response is a generic An error occurred in the Server Components render message plus a short Digest: 1234567890 hash. We built a minimal app that throws on purpose, ran it with next build && next start, and compared what the browser gets against what the terminal running the server prints.

1. The setup we tested

A one-route app on Next.js 16.3.8:

// app/page.js
export const dynamic = "force-dynamic";

export default async function Page() {
  throw new Error("secret db password is hunter2");
}

export const dynamic = "force-dynamic" matters: without it, Next.js tries to prerender the page at build time and the build itself fails with the full error printed to the build log. With it, the error happens per-request, which is the case developers actually hit in production (a route that throws only for some users/inputs).

We ran next build (succeeded) then next start -p 8977, and hit the route with curl.

2. What the browser actually receives

The response is HTTP 500, and the body is a React Server Components streaming payload, not a plain HTML error page. The relevant line in that payload was, byte for byte:

4:E{"digest":"2006440691"}

That is the entire error object serialized to the client: only a digest field. There is no message key at all, not even a sanitized placeholder string, in the raw payload. The generic "An error occurred in the Server Components render" text you see in a browser comes from Next.js's default client-side error UI filling in a fallback when it finds a digest with no message — it is not something the server sent as text.

We repeated the request 3 times against the same running server process. The digest was identical every time: 2006440691. So within one running server instance (one build, one `next start`), the digest is a stable fingerprint of that specific error — useful for grep-ing logs. It is not a per-request random value.

Note it is not stable across builds: the same throw during `next build`'s own prerender attempt (a separate process invocation) produced a different digest (2775923925). Don't assume a digest you saw yesterday still matches today's deploy.

3. What the terminal running next start shows

At the same moment the browser got only a digest, the server process's own stdout printed the full error, every time:

⨯ Error: secret db password is hunter2
    at b (.next/server/chunks/ssr/_0tazdgc._.js:1:65) {
  digest: '2006440691'
}

The real message and a stack trace (pointing at the compiled server chunk, not your original source line, since this is a production build) are only ever in server-side logs: the terminal if you're self-hosting with next start, or the function logs in your hosting provider's dashboard if deployed. The browser and any client-side JavaScript never see that text. This is deliberate: Next.js is avoiding leaking server internals (stack traces, file paths, secrets in error messages) to end users.

4. Catching it with error.tsx

Add an app/error.tsx client component to render something better than the default page:

"use client";

export default function Error({ error, reset }) {
  return (
    <div>
      <p>Something broke. Reference: {error.digest}</p>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

It receives error.digest and error.message as props. Based on the raw payload in section 2, for a Server Component error in production, error.message arrives as undefined in the error boundary too — there is no message field on the wire for it to read. Only error.digest is reliable. Showing the digest to the user (and asking them to report it) is what lets you correlate their bug report with the matching line in your server logs.

One gap this boundary doesn't cover: app/error.tsx only catches errors in pages and nested layouts, never in the root app/layout.tsx itself — we measured that case separately in why app/error.tsx doesn't catch root layout errors, and how app/global-error.tsx fixes it.

5. The actual fix

There is no config flag that puts the real message back in production (and you don't want one — that's the leak this behavior prevents). Two real options:

If you only need the message during your own testing, run next dev instead of a production build: dev mode shows the full error and stack directly in the browser by design, with no digest stripping.

6. Quick reference

WhereWhat you see
Browser, production (next start / deployed)HTTP 500, generic message, Digest: <hash> only
Browser, error.tsx props, productionerror.digest populated, error.message undefined
Server terminal / host function logs, productionFull Error: <real message> plus stack, tagged with the same digest
Browser, next devFull error overlay with message and stack, no stripping

Frequently asked questions

What does the Digest number in a Next.js error mean?

It's a hash Next.js generates for an error thrown in a Server Component, Route Handler, or Server Action during a production build. We measured it as stable across repeated requests to the same running server, so you can use it to find the matching full error in your server-side logs.

Why doesn't Next.js show me the real error message in production?

By design. Server-side errors can contain stack traces, file paths, or data that shouldn't reach a browser. Next.js strips the message and sends only the generic text plus a digest. We confirmed the raw response contains no message field at all, only digest.

Does error.tsx give me the real error.message?

No, not for this case. We added an error.tsx boundary and the serialized error payload it receives has no message field for a production Server Component error, only digest. error.message comes through undefined.

Where is the real error logged?

On the machine or process running next start: its stdout. We saw the full Error object with message and stack printed there every time, in sync with the digest shown to the browser. On managed hosts, check that host's function/runtime log viewer instead.

Is the digest the same every time the same bug happens?

Within one running server build, yes — we got the identical digest across 3 repeated requests. Across a different process invocation (we compared a build-time prerender attempt to the running server), the digest changed, so don't treat it as a permanent ID across deploys.

Written with AI assistance. Measured on 2026-10-03 with Node.js 24.21.0 and Next.js 16.3.8 (App Router, Turbopack). Built a minimal app with a Server Component that throws, ran `next build && next start` in production mode, and inspected the raw HTTP response with curl plus the `next start` terminal output directly. PlainHeaders is our extension; curl and Next.js are free.