Home

Fix this error · React and Next.js

Fix: An error occurred in the Server Components render

Next.js hides the real Server Component error in production. Find it by digest, fix the usual causes, and report it with onRequestError.

A Server Component threw on the server. In production React replaces the real message with this generic one before it reaches the browser, so the browser text tells you nothing about the cause. The fix starts on the server: find the original error in your server logs by its digest, or report it from onRequestError in instrumentation.ts, then fix that error.

FactDetail
ErrorAn 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.
Where you see itBrowser console, your error.tsx boundary, the built-in "This page couldn't load" page with ERROR <digest> at the bottom
Usual causeA Server Component, layout or Server Action threw: a failed fetch or database call, a missing environment variable, or undefined data
Usual fixRead the real error on the server, then handle the failure or return it as data
BugTape catches itYes, automatically on the server, with the Node SDK in onRequestError (Node.js runtime)

What the error means

In the App Router, pages and layouts are Server Components by default. They run on the server, and React streams the result to the browser. When one of them throws, React records the error on the server and sends the browser a stand-in error. In development the stand-in keeps the original message. In production React drops the message on purpose, because it can contain secrets, SQL or customer data.

The stand-in has one useful property, digest. It is a hash that Next.js also logs next to the original error on the server. The Next.js docs say to use it "to match the corresponding server-side logs".

So this message is never the bug. It is a pointer to a server error you have not looked at yet.

Common causes

  1. A data call failed. A fetch, database query or SDK call inside an async Server Component threw or returned an error status that the code then treated as data.
  2. The data was missing. The code read a property of undefined or null, such as post.author.name for a deleted author.
  3. An environment variable is missing in production. It exists in .env.local but not in the hosting settings, so a client constructor throws.
  4. A Server Action threw. An unhandled error inside a 'use server' function reaches the client the same way.
  5. A non-serializable prop crossed to a Client Component. Passing a function or a class instance from a Server Component to a Client Component throws Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". or Only plain objects can be passed to Client Components from Server Components., and production hides both behind the generic message.

How to fix it

Find the original error

Development shows the real message in the overlay. To reproduce the production case, run a production build locally and watch the terminal: the server prints the original error there.

next build && next start

On a host, search the function or server logs for the digest value. The line with that digest has the original message and stack.

Handle failed data calls

Check the response before you use it, and decide what the page shows when the call fails. Call notFound() for a missing record so Next.js renders a 404 instead of an error.

// app/posts/[id]/page.tsx
import { notFound } from 'next/navigation';

export default async function PostPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const res = await fetch(`${process.env.API_URL}/posts/${id}`);

  if (res.status === 404) notFound();
  if (!res.ok) throw new Error(`Post API answered ${res.status}`);

  const post = await res.json();
  return <h1>{post.title}</h1>;
}

The thrown error still shows the generic message in the browser. The difference is that the server log now says what failed.

Guard optional data

// Before: throws when the author was deleted
<p>{post.author.name}</p>

// After
<p>{post.author?.name ?? 'Unknown author'}</p>

Fail early on missing environment variables

Read required variables once, in a server-only module, and throw a clear message. Then add the variable in your host's settings and redeploy.

// lib/env.ts
import 'server-only';

export function requireEnv(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing environment variable ${name}`);
  return value;
}

Return expected errors from Server Actions

The Next.js docs recommend returning expected errors as values and reading them with useActionState. Throw only for errors you did not expect.

'use server';

export async function createPost(prevState: { message: string }, formData: FormData) {
  const res = await fetch(`${process.env.API_URL}/posts`, {
    method: 'POST',
    body: JSON.stringify({ title: formData.get('title') }),
  });
  if (!res.ok) return { message: 'Could not save the post. Try again.' };
  return { message: '' };
}

Pass plain data to Client Components

Pass strings, numbers, dates, plain objects and arrays. Turn class instances, such as an ORM model, into plain objects first. Mark a function with 'use server' if the client must call it.

// Before: a class instance and a function cross the boundary
<PostCard post={postModel} onShare={() => share(postModel.id)} />

// After: plain data; PostCard calls a 'use server' action to share
<PostCard post={{ id: postModel.id, title: postModel.title }} />

Show a useful fallback

Add error.tsx next to the page so one failed segment does not take down the whole layout. Since Next.js 16.3 the boundary receives retry, which re-fetches and re-renders the segment. Older versions pass reset instead.

// app/posts/[id]/error.tsx
'use client';

export default function Error({ error, retry }: { error: unknown; retry: () => void }) {
  const digest = error instanceof Error && 'digest' in error ? String(error.digest) : undefined;
  return (
    <div>
      <p>This post could not load.</p>
      {digest && <p>Reference: {digest}</p>}
      <button onClick={() => retry()}>Try again</button>
    </div>
  );
}

How to catch it in production with BugTape

The useful report for this error comes from the server, where the real message still exists. Next.js 15 and later call onRequestError in instrumentation.ts for errors in pages, layouts, route handlers and Server Actions. The BugTape Node SDK reports from that hook, with the route pattern (/posts/[id]) and the route type:

// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') (await import('@bugtape/node')).init({ service: 'web' });
}

export async function onRequestError(...args: Parameters<typeof import('@bugtape/node').captureRequestError>) {
  if (process.env.NEXT_RUNTIME === 'nodejs') await (await import('@bugtape/node')).captureRequestError(...args);
}

Install it with npm install @bugtape/node and set BUGTAPE_KEY in your host's environment. The report carries the error type, the redacted message and the stack frames in your code. Next.js redirect and not-found signals are skipped. captureRequestError waits up to 2 seconds for delivery, so the report leaves before a serverless function is frozen. Details are in the Node.js guide.

What it does not catch automatically:

  • Routes on the Edge runtime. The checks above skip Edge, and the SDK does not support it. Move the route to the Node.js runtime or report from your own code.
  • Errors you catch yourself. If a try/catch in a Server Component swallows the error, onRequestError never sees it. Add bugtape.captureException(err) in the catch.
  • Errors at build time. A page that fails while next build prerenders it fails the build. Nothing is running yet to report it.

The browser side is optional. With the browser SDK installed, the generic message reaches it only when no error.tsx catches the error: Next.js then passes it to reportError(), which fires the window error event the SDK listens for. Your own error.tsx only logs to the console, which BugTape records but which does not start a report. That browser report would only carry the generic text, so the server report is the one to read.

Repeats of the same server error group into one issue with a count. Set release (the SDK fills it from VERCEL_GIT_COMMIT_SHA and other common commit variables) so that when you mark the issue fixed, a report from a newer release reopens it as a regression. Your coding agent can read the issue over MCP (npx -y bugtape connect claude, or cursor or codex) and propose the fix. You review the pull request.

FAQ

What is the digest in a Next.js error?

It is a hash that Next.js generates for the thrown error. The browser gets only the digest and the generic message. The server log has the same digest next to the original message and stack, so you can match a user's report to the real error.

Why does it work in development but fail in production?

Often the same error happens in both. Development shows the real message and production hides it. When the error really happens only in production, the usual causes are an environment variable that is missing on the host, production data that has a null the test data did not, or an API that rejects the production server's requests.

Can I show the real error message to the user?

Not for errors thrown in Server Components. Production builds remove the message on purpose. If the user should see a reason, return it as data, for example a { message } value from a Server Action read with useActionState, or render a fallback in the page.

Does error.tsx catch errors in layout.tsx?

Not the layout in the same folder. error.tsx wraps the page and nested layouts below it. An error in a layout is caught by the error.tsx of the parent segment, and an error in the root layout needs app/global-error.tsx.

Sources

All guides: Fix an error.

Start free. Free plan · No credit card · Set up in minutes. Or try the live demo first.