The first time you ship a React Server Components page to production, an error is going to throw inside an async server function and you are going to discover that your existing Error Boundary did absolutely nothing. The classic ErrorBoundary class component is a client construct. It catches errors thrown during render in components below it in the tree. Server Components do not render in the browser. By the time the error reaches the boundary, it is already a hydration error, not the original failure.
This trips up almost every team migrating from Pages Router or a non-RSC stack. The fix is not complicated, but it is not obvious if you are reading the React docs without context for what changed.
Why Error Boundaries cannot catch RSC errors
An Error Boundary uses componentDidCatch or the equivalent function-component pattern with getDerivedStateFromError. Both run during the React render cycle in the browser. RSC content is rendered on the server, serialized to a special wire format, and streamed to the client. The client React runtime reconstructs the tree from that stream. Errors that throw on the server during the render or async data fetching never enter the client render cycle as errors. They arrive as a serialized error payload or, worse, as a streaming abort with no recoverable context.
This is by design. Letting a server-side stack trace flow to the browser would leak file paths, environment details, and potentially user data. Frameworks must catch server errors, sanitize them, and decide what the client gets to see.
What Next.js gives you - error.tsx
Next.js App Router solves the gap with the error.tsx file convention. Drop one in any route segment and it becomes the error boundary for that segment, including the Server Components inside it. The file exports a default React component that receives error and reset props.
The component itself is a client component (it must be marked "use client" because the reset function is interactive). The framework handles the bridge: it catches the server error, serializes a sanitized version, and renders your error.tsx in place of the failed segment. Your component can show a friendly message, log to your error tracking service, and offer a retry.
The trap is that error.tsx only catches errors inside that segment's Server Components and any nested layouts and pages. Errors thrown in the layout itself are not caught by error.tsx in the same directory. You need global-error.tsx at the root for that.
The hybrid pattern that covers both sides
Pure error.tsx is enough for read-only RSC pages. Once you add interactive client components, you need both layers.
- error.tsx at each route segment catches Server Component errors before they reach the client. This is your last line of defense against unsanitized server errors leaking out.
- A classic Error Boundary inside client component trees catches errors during interactive renders (state updates, effects, event handlers). React itself does not bubble effect errors into error.tsx, so without an explicit boundary they crash the whole tree.
- An async error wrapper for data fetching in client components using try-catch in the async function plus a state flag that flips the component into an error mode. RSC handles fetching declaratively, but client components still need defensive try-catch around fetch calls.
Logging the right level of detail
The temptation is to log the full error to your tracking service from inside error.tsx. That is wrong because by the time error.tsx renders, you only have the sanitized error. The full server error and stack trace are gone.
The correct place to log server errors is on the server, before the framework sanitizes them. In Next.js this means hooking the error event in your instrumentation file or wrapping your server-side logic in try-catch with explicit logger calls. Sentry and Bugsnag both ship Next.js integrations that do this automatically. Without one of those (or a hand-written wrapper), your error tracking will be missing the half of the errors that throw during RSC render.
The mental model that helps: Server Component errors flow through the framework. Client Component errors flow through React. You need a catcher on each path or one of them goes silent.
The reset function and what it actually resets
error.tsx receives a reset function that re-renders the segment. The first time you use it you will assume it triggers a fresh fetch. It does not. Reset re-mounts the React tree but uses the cached RSC payload. If the underlying data is still broken, you re-render the same error.
For real recovery you need to either invalidate the cache (Next.js: revalidatePath or revalidateTag from a server action) or use router.refresh() from the client to fetch fresh server data. Reset is a UI reset, not a data reset. Building this correctly is one of the patterns we encode into our white-label engineering playbook so client teams ship reliable error UX without rediscovering the gotcha.
What to test before you ship
Three failure modes should have explicit tests in your repo before any RSC-heavy page goes to production.
- Server-side throw in a Server Component. Force an exception inside an async data fetch. Confirm error.tsx renders, the error is logged to your tracking service, and reset attempts produce a recoverable state when the underlying issue resolves.
- Client-side throw in a hydrated component. Force an error inside a state setter or event handler. Confirm your client Error Boundary catches it and the rest of the page stays interactive.
- Network failure during streaming. Cut the connection partway through a streamed RSC response. Confirm the user sees a meaningful error rather than a half-rendered page.
The third one catches the most production bugs because it is the one nobody thinks to test in dev. Streaming responses fail in real networks more often than developers assume.
Make error handling explicit, not implicit
The pattern that breaks teams is assuming React's error story is the same in App Router as it was in the rest of React. It is not. RSC requires you to think about two render phases with different error contracts. Once you have the mental model, the code is straightforward. Skip the model and you will ship apps that look fine in development and break in subtle ways the first time something goes wrong in production. We help our portfolio clients set up the full error boundary architecture during the initial build so it is not a retrofit later.