Skip to the solution
MediumFrontend7 min read

Error Boundaries with RSC — why can't Server Components catch errors?

80 views

The problem

Question presented to candidate: "Why does the error boundary in a React Server Components app have to be a Client Component?"

What a strong answer should cover:

  • An Error Boundary needs getDerivedStateFromError, componentDidCatch and state to hold the error. A Server Component has no state, no lifecycle and renders once — so it cannot be one.
  • Boundaries therefore live on the client, marked "use client", and they can still wrap Server Components rendered as their children, because composition crosses the boundary even though code does not.
  • When a Server Component throws on the server, the framework serialises the failure into the stream and the nearest client boundary renders its fallback.
  • In production the message is redacted. You get a digest — a hash to correlate with your server logs — not the original message, because a server error can contain a query, a path, or a secret.
  • That is a genuine debugging difference: server errors are found in your server logs, not in the browser console.
  • Recovery is different too. A client boundary cannot re-run a Server Component's render on its own — retrying needs a new request to the server, which is what a framework's reset does.
  • In Next.js the App Router convention is an error.tsx file, which must carry "use client" for exactly this reason.
  • Server-side errors that never reach a boundary — a route handler, a failed Server Action — need server-level observability instead.

Clarifying questions expected:

  • "Did the error happen during the server render, or after hydration on the client?" — different diagnosis entirely.
  • "Do we have server-side error reporting wired up?"

Code / implementation expected: Optional. Pointing out the "use client" on the boundary is the substance.

  • error-boundary
  • rsc

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read · 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactThe boundary that must be a Client Component, and a retry that actually retries
import React, { Component, useState } from "react";

// This playground has no RSC runtime, so the SERVER side below is annotated
// reference. What runs live is the client boundary itself — the half that is
// identical in an RSC app.

const SERVER_SIDE = `// app/report/page.jsx  — a Server Component, no "use client"
export default async function ReportPage() {
  const rows = await db.report.findMany();   // may throw, ON THE SERVER
  return (
    // The boundary is a CLIENT component, imported here and wrapping
    // Server Components as children. Composition crosses the boundary
    // even though code does not.
    <ErrorBoundary>
      <ReportTable rows={rows} />            {/* still a Server Component */}
    </ErrorBoundary>
  );
}

// app/report/error.jsx  — the Next.js App Router convention
"use client";                                //  REQUIRED: needs state
export default function Error({ error, reset }) {
  // In production, error.message is generic and error.digest is a hash
  // you match against the server logs. The real text never leaves the server.
  return (
    <div>
      <p>Something went wrong.</p>
      <code>digest: {error.digest}</code>
      {/* reset() RE-REQUESTS the segment from the server. A setState-only
          retry would re-render the same failed payload forever. */}
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}`;

// ── The live half: a boundary needs state and a SECOND render ──────────────
class ErrorBoundary extends Component {
  state = { error: null };
  static getDerivedStateFromError(error) { return { error }; }
  render() {
    if (this.state.error) {
      return (
        <div style={{ border: "1px solid #e0b4b4", background: "#fdf0f0", borderRadius: 8, padding: 12 }}>
          <strong style={{ color: "#a33" }}>fallback rendered</strong>
          <div style={{ fontSize: 13 }}>{this.state.error.message}</div>
          {/* Clearing state only helps because the CHILD is client-side and
              will genuinely re-run. For a server payload it would not. */}
          <button onClick={() => this.setState({ error: null })}>reset boundary state</button>
        </div>
      );
    }
    return this.props.children;
  }
}

function Child({ broken }) {
  if (broken) throw new Error("the report query failed");
  return <div style={{ background: "#f2f9f2", border: "1px solid #cde3cd", borderRadius: 8, padding: 12 }}>report rendered fine</div>;
}

export default function App() {
  const [broken, setBroken] = useState(false);

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 620 }}>
      <button onClick={() => setBroken((b) => !b)} style={{ marginBottom: 10 }}>
        {broken ? "fix the child" : "break the child"}
      </button>

      <ErrorBoundary key={String(broken)}>
        <Child broken={broken} />
      </ErrorBoundary>

      <p style={{ fontSize: 13, color: "#666" }}>
        The boundary above holds the error in <code>state</code> and renders a
        different tree — two things a Server Component cannot do, which is the
        whole answer to the question. Below is what the server half looks like.
      </p>

      <pre style={{ background: "#f6f6f8", border: "1px solid #ddd", borderRadius: 8,
                    padding: 12, fontSize: 12, overflowX: "auto", whiteSpace: "pre-wrap" }}>
{SERVER_SIDE}
      </pre>
    </div>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 69 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextMedium

Explain React Suspense and its use cases.

Continue