React Error Boundaries mit Claude Code sicher implementieren
React Error Boundaries mit Claude Code: Fangbereich, Platzierung, Reset, sichere Logs, Tests und Prompts.
Der gefaehrlichste Frontend-Fehler ist nicht ein einzelnes kaputtes Diagramm. Gefaehrlich wird es, wenn eine kleine Render-Ausnahme die gesamte React-App in eine leere Seite verwandelt. Wenn du Claude Code nur bittest, “Fehlerbehandlung hinzuzufuegen”, entstehen oft ein paar try/catch-Bloecke, waehrend Render-Fehler weiterhin nicht sauber isoliert sind.
Dieser Leitfaden zeigt, wie du React Error Boundaries mit Claude Code sicher implementierst. Ein Error Boundary ist eine Grenze im Komponentenbaum: Wenn ein Kind waehrend des Renderns unerwartet wirft, zeigt die Grenze eine Fallback-UI, statt den ganzen Baum abstuerzen zu lassen. Sie behebt den Bug nicht. Sie begrenzt den Schaden, gibt Nutzern einen naechsten Schritt und erzeugt einen verwertbaren Logeintrag.
Masa hat das in einem Admin-Dashboard getestet. Der erste Prompt packte die ganze App in eine einzige Boundary. Die leere Seite verschwand, aber ein kaputtes Umsatzdiagramm verdeckte auch Einstellungen und Billing, und die Logs enthielten URLs mit E-Mail-Adressen in Query Strings. Erst als der Prompt Platzierung, Reset-Regeln, PII-Redaktion und Tests verlangte, wurde der von Claude Code erzeugte Diff gut reviewbar.
Fakten Mit Der React-Dokumentation Festlegen
Bevor Claude Code Code schreibt, muessen die Fakten feststehen. Die offizielle React-Referenz zu Component beschreibt die Rollen: static getDerivedStateFromError setzt den Zustand fuer die Fallback-UI, componentDidCatch ist fuer Seiteneffekte wie Logging gedacht. Die offizielle error-boundaries Lint-Dokumentation macht ausserdem klar, dass normales try/catch um JSX nicht das richtige Werkzeug fuer Render-Fehler ist.
Die wichtigste Einschraenkung: Error Boundaries fangen nicht alles. Sie fangen unerwartete Fehler aus Kindkomponenten waehrend Rendering, Lifecycles oder Code, der im Renderfluss laeuft. Sie fangen keine Click-Handler, Timer, normale Promise-Rejections, serverseitiges Rendering und keine Fehler in der Boundary selbst.
| Ort des Fehlers | Von Error Boundary gefangen | Produktionsbehandlung |
|---|---|---|
| Kindkomponente wirft beim Rendern | Ja | Fallback-UI anzeigen und redigierten Log senden |
| Hook oder Memo wirft im Renderfluss | Meist ja | Erwartete Fehler vorher validieren, unerwartete Ausnahmen an Boundary geben |
| Button-Click oder Formular-Submit | Nein | Lokal mit try/catch behandeln, bei Bedarf ueber State erneut werfen |
setTimeout, requestAnimationFrame oder normale Promise | Nein | Promise explizit behandeln und Retry anbieten |
| Server-Side Rendering | Nein | Framework-Fehlerseite, Serverlogs und HTTP-Status nutzen |
| Fehler im Fallback der Boundary | Nein | Fallback einfach halten und hoehere Boundary setzen |
flowchart TD
A["Kindkomponente wirft beim Rendern"] --> B["Naechste Error Boundary"]
B --> C["Fallback-UI fuer Nutzer"]
B --> D["Redigierter Fehlerbericht"]
E["Click-Handler oder setTimeout wirft"] --> F["Lokal behandeln oder ueber State weitergeben"]
F --> B
Route-Level Und Component-Level Trennen
Mehr Boundaries bedeuten nicht automatisch bessere Architektur. Eine einzige Boundary um die App ist zu breit, aber eine Boundary um jeden Button erzeugt nur Rauschen und verwirrende Fallback-Fragmente. Claude Code sollte explizit wissen, wo Route-Level-Boundaries und wo Component-Level-Boundaries hingehoren.
Eine Route-Level-Boundary schuetzt eine Bildschirmverantwortung: Dashboard, Einstellungen, Billing, Editor, Suche oder Admin-Audit-Log. Sie sollte bei Navigation resetten, damit ein Fehler der vorherigen Route nicht in die naechste Ansicht mitwandert.
Eine Component-Level-Boundary schuetzt eine unabhaengige Region innerhalb einer Seite. Gute Kandidaten sind Umsatzdiagramme, Benachrichtigungspanels, Markdown-Previews, Empfehlungswidgets, Third-Party-Embeds oder schwere JSON-Viewer. Schlechte Kandidaten sind normale Inputs, Submit-Buttons, Ueberschriften und Icons. Diese gehoeren zu Validierung und normalem UI-State.
Nutze drei Fragen: Kann der Nutzer weiterarbeiten, wenn diese Region ausfaellt; kann die Region separat retry, reload oder reset ausfuehren; macht ein Feature-Name im Log die Diagnose leichter. Das passt zu Claude Code Testing Strategies: Die Einheit, die der Nutzer wiederholen kann, sollte auch testbar sein.
Kopierbare Error Boundary Komponente
Die gemeinsame Error Boundary bleibt eine Class Component. Die restliche App kann weiterhin aus Function Components bestehen; nur diese Schicht braucht die React-Lifecycle-Methoden fuer Boundaries.
// src/components/error-boundary/ErrorBoundary.tsx
import { Component, ErrorInfo, ReactNode } from "react";
export type ErrorBoundaryFallbackProps = {
error: Error;
resetErrorBoundary: () => void;
};
type ErrorBoundaryProps = {
children: ReactNode;
fallback?: ReactNode | ((props: ErrorBoundaryFallbackProps) => ReactNode);
onError?: (error: Error, info: ErrorInfo) => void;
onReset?: () => void;
resetKeys?: ReadonlyArray<unknown>;
};
type ErrorBoundaryState = {
error: Error | null;
};
function normalizeError(value: unknown): Error {
if (value instanceof Error) return value;
return new Error(typeof value === "string" ? value : "Unknown render error");
}
function changedArray(
previous: ReadonlyArray<unknown> = [],
next: ReadonlyArray<unknown> = [],
): boolean {
return (
previous.length !== next.length ||
previous.some((item, index) => !Object.is(item, next[index]))
);
}
export class ErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
state: ErrorBoundaryState = { error: null };
static getDerivedStateFromError(error: unknown): ErrorBoundaryState {
return { error: normalizeError(error) };
}
componentDidCatch(error: Error, info: ErrorInfo) {
this.props.onError?.(normalizeError(error), info);
}
componentDidUpdate(previousProps: ErrorBoundaryProps) {
if (
this.state.error &&
changedArray(previousProps.resetKeys, this.props.resetKeys)
) {
this.resetErrorBoundary();
}
}
resetErrorBoundary = () => {
this.props.onReset?.();
this.setState({ error: null });
};
render() {
if (!this.state.error) return this.props.children;
if (typeof this.props.fallback === "function") {
return this.props.fallback({
error: this.state.error,
resetErrorBoundary: this.resetErrorBoundary,
});
}
if (this.props.fallback) return this.props.fallback;
return (
<section role="alert" aria-labelledby="error-boundary-title">
<h2 id="error-boundary-title">Something went wrong</h2>
<p>Please retry. If the problem continues, contact support.</p>
<button type="button" onClick={this.resetErrorBoundary}>
Try again
</button>
</section>
);
}
}
fallback kann ein statischer Node oder eine Funktion sein. Die Funktionsform ist praktischer, weil sie error und resetErrorBoundary bekommt. Zeige aber niemals error.stack, rohe API-Antworten oder interne Debugtexte in der UI. Nutzer brauchen eine kurze Erklaerung, eine sichere Aktion und eventuell eine Support-Referenz.
Fallback UI, Reset Und Retry
Fallback-UI ist Produkt-UI, kein Debug-Dump. Sie sollte sagen, welcher Teil nicht funktioniert, ob Nutzerdaten geaendert wurden und welche Aktion sicher ist. Bei Chunk-Loading-Fehlern nach einem Deploy kann ein kompletter Reload helfen. Bei einem normalen Widget ist ein Retry nur fuer die Region weniger stoerend.
// src/components/error-boundary/AppErrorFallback.tsx
import type { ErrorBoundaryFallbackProps } from "./ErrorBoundary";
export function AppErrorFallback({
error,
resetErrorBoundary,
}: ErrorBoundaryFallbackProps) {
const reloadRecommended =
/ChunkLoadError|Loading chunk|dynamically imported module/i.test(
error.message,
);
return (
<section
role="alert"
aria-labelledby="app-error-title"
className="error-fallback"
>
<div>
<p className="error-fallback__eyebrow">This section stopped working</p>
<h2 id="app-error-title">We could not render this part of the page.</h2>
<p>
Your account data was not changed. Retry this section first, then
reload the app if the same message appears again.
</p>
</div>
<div className="error-fallback__actions">
<button type="button" onClick={resetErrorBoundary}>
Try again
</button>
{reloadRecommended ? (
<button type="button" onClick={() => window.location.reload()}>
Reload app
</button>
) : null}
</div>
</section>
);
}
/* src/components/error-boundary/error-fallback.css */
.error-fallback {
border: 1px solid #d7dde8;
border-radius: 8px;
padding: 16px;
background: #fff;
color: #1f2937;
}
.error-fallback__eyebrow {
margin: 0 0 4px;
color: #6b7280;
font-size: 0.875rem;
}
.error-fallback h2 {
margin: 0 0 8px;
font-size: 1.125rem;
}
.error-fallback__actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
Der typische Retry-Fehler: Man loescht nur den Boundary-State, aber die kaputte Eingabe bleibt gleich. Wenn dieselben Props, derselbe Cache oder derselbe Route-State sofort wieder werfen, wirkt der Button defekt. Verwende resetKeys fuer Route Keys, Filter, User IDs, Refresh-Zaehler oder Datenversionen.
Beispiele Fuer Route Und Komponente
Mit React Router kann die Route-Boundary ein schlanker Wrapper sein. Dieses Beispiel resettet mit location.key und meldet einen Feature-Namen. Next.js und Remix haben eigene Route-Error-Dateien, aber die Designentscheidung bleibt gleich: bei Navigation resetten und Seitenfehler isolieren.
// src/AppRoutes.tsx
import { lazy, ReactNode, Suspense } from "react";
import {
createBrowserRouter,
RouterProvider,
useLocation,
} from "react-router-dom";
import { ErrorBoundary } from "./components/error-boundary/ErrorBoundary";
import { AppErrorFallback } from "./components/error-boundary/AppErrorFallback";
import { currentErrorContext, reportReactError } from "./lib/error-reporting";
import { Layout } from "./routes/Layout";
const DashboardPage = lazy(() => import("./routes/DashboardPage"));
const SettingsPage = lazy(() => import("./routes/SettingsPage"));
function RouteBoundary({
children,
feature,
}: {
children: ReactNode;
feature: string;
}) {
const location = useLocation();
return (
<ErrorBoundary
resetKeys={[location.key]}
fallback={(props) => <AppErrorFallback {...props} />}
onError={(error, info) => {
void reportReactError(
error,
info.componentStack,
currentErrorContext(feature),
);
}}
>
<Suspense fallback={<p>Loading...</p>}>{children}</Suspense>
</ErrorBoundary>
);
}
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
children: [
{
path: "dashboard",
element: (
<RouteBoundary feature="dashboard">
<DashboardPage />
</RouteBoundary>
),
},
{
path: "settings",
element: (
<RouteBoundary feature="settings">
<SettingsPage />
</RouteBoundary>
),
},
],
},
]);
export function AppRoutes() {
return <RouterProvider router={router} />;
}
Component-Level-Boundaries gehoeren um unabhaengig wiederherstellbare Regionen: Diagramme, Markdown-Previews, Empfehlungspanels, Third-Party-Embeds und JSON-Viewer. Sie gehoeren nicht um jedes Formularfeld. Zahlungsfehler, Validierungsfehler oder abgelaufene Sessions sind normale Produktzustaende.
Async-Fehler Und Event Handler
Error Boundaries fangen Click-Handler und normale Async-Fehler nicht automatisch. Erwartete Fehler bleiben lokal: Feldvalidierung, Authentifizierung, Zahlungsablehnung. Unerwartete Ausnahmen koennen in State gespeichert und beim naechsten Render geworfen werden, damit sie die naechste Boundary erreichen.
// src/components/error-boundary/useAsyncBoundary.ts
import { useCallback, useState } from "react";
function toError(value: unknown): Error {
if (value instanceof Error) return value;
return new Error(typeof value === "string" ? value : "Unknown async error");
}
export function useAsyncBoundary() {
const [error, setError] = useState<Error | null>(null);
if (error) {
throw error;
}
return useCallback((value: unknown) => {
setError(toError(value));
}, []);
}
// src/components/settings/SaveButton.tsx
import { useState } from "react";
import { useAsyncBoundary } from "../error-boundary/useAsyncBoundary";
type SaveButtonProps = {
onSave: () => Promise<void>;
};
export function SaveButton({ onSave }: SaveButtonProps) {
const [pending, setPending] = useState(false);
const throwToBoundary = useAsyncBoundary();
async function handleClick() {
setPending(true);
try {
await onSave();
} catch (error) {
throwToBoundary(error);
} finally {
setPending(false);
}
}
return (
<button type="button" disabled={pending} onClick={handleClick}>
{pending ? "Saving..." : "Save"}
</button>
);
}
Schreibe in den Claude-Code-Prompt, dass nicht jeder Async-Fehler an die Boundary gehen darf. Ein 400 Response, ein ungueltiges Feld, Rate Limit oder abgelaufene Session braucht lokale UI. Die Boundary ist fuer unerwartete Ausnahmen, kaputte Antworten und Render-Annahmen, die sonst die Seite leeren wuerden.
Logging Ohne PII-Lecks
PII sind personenbezogene Daten: E-Mail, Telefonnummer, Name, Adresse, Tokens, Kartennummern oder Freitext aus Supportformularen. componentDidCatch ist ein guter Ort fuer Client-Error-Reporting, aber nur mit begrenztem und redigiertem Payload.
Logge Feature, Release, Route Pathname, Error Name, redigierte Message, Stack und Component Stack. Sende keine Query Strings, Formularwerte, Cookies, Authorization Headers, rohen API-Antworten oder vollstaendigen URLs.
// src/lib/error-reporting.ts
type ClientErrorContext = {
route: string;
release: string;
feature?: string;
userHash?: string;
};
const REDACTIONS: Array<[RegExp, string]> = [
[/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "[redacted-email]"],
[/\b(?:\d[ -]*?){13,19}\b/g, "[redacted-number]"],
[/\b(token|secret|password|authorization)=([^&\s]+)/gi, "$1=[redacted]"],
[/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]"],
];
export function redactText(value: string | undefined): string | undefined {
if (!value) return value;
return REDACTIONS.reduce(
(text, [pattern, replacement]) => text.replace(pattern, replacement),
value,
);
}
export function currentErrorContext(feature?: string): ClientErrorContext {
const env = (import.meta as unknown as {
env?: Record<string, string | undefined>;
}).env;
return {
route: typeof window === "undefined" ? "server" : window.location.pathname,
release: env?.VITE_APP_VERSION ?? "dev",
feature,
};
}
export async function reportReactError(
error: Error,
componentStack: string | undefined,
context: ClientErrorContext,
) {
const payload = {
name: redactText(error.name) ?? "Error",
message: redactText(error.message) ?? "Unknown error",
stack: redactText(error.stack),
componentStack: redactText(componentStack),
route: context.route,
release: context.release,
feature: context.feature,
userHash: context.userHash,
};
const body = JSON.stringify(payload);
if (typeof navigator !== "undefined" && navigator.sendBeacon) {
const sent = navigator.sendBeacon(
"/api/client-errors",
new Blob([body], { type: "application/json" }),
);
if (sent) return;
}
await fetch("/api/client-errors", {
method: "POST",
headers: { "content-type": "application/json" },
credentials: "omit",
keepalive: true,
body,
});
}
Redigiere auf dem Server erneut. Clientseitige Redaction hilft, ist aber keine Compliance-Grenze. Claude Code sollte beide Schichten implementieren und nur gehashte User-Identifier verwenden, falls Support-Korrelation noetig ist.
Tests Und Verifikationsbefehle
Eine Error Boundary ist nur im Fehlerfall relevant, also muss der kaputte Pfad getestet werden. Minimum: Fallback wird angezeigt, onError wird aufgerufen, Retry kann resetten.
// src/components/error-boundary/ErrorBoundary.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom/vitest";
import { ReactNode, useState } from "react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ErrorBoundary } from "./ErrorBoundary";
function Bomb({ shouldThrow }: { shouldThrow: boolean }) {
if (shouldThrow) {
throw new Error("profile widget crashed");
}
return <p>Profile loaded</p>;
}
function RetryHarness({ onError }: { onError: ReturnType<typeof vi.fn> }) {
const [broken, setBroken] = useState(true);
return (
<ErrorBoundary
onError={onError}
fallback={({ resetErrorBoundary }) => (
<button
type="button"
onClick={() => {
setBroken(false);
resetErrorBoundary();
}}
>
Retry profile
</button>
)}
>
<Bomb shouldThrow={broken} />
</ErrorBoundary>
);
}
function StaticFallback({ children }: { children: ReactNode }) {
return (
<ErrorBoundary fallback={<p>Could not load this panel.</p>}>
{children}
</ErrorBoundary>
);
}
describe("ErrorBoundary", () => {
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
});
afterEach(() => {
consoleErrorSpy.mockRestore();
});
it("renders fallback UI when a child throws", () => {
render(
<StaticFallback>
<Bomb shouldThrow />
</StaticFallback>,
);
expect(screen.getByText("Could not load this panel.")).toBeInTheDocument();
});
it("calls onError with the thrown error and component stack", () => {
const onError = vi.fn();
render(<RetryHarness onError={onError} />);
expect(onError).toHaveBeenCalledTimes(1);
expect(onError.mock.calls[0][0].message).toBe("profile widget crashed");
expect(onError.mock.calls[0][1].componentStack).toContain("Bomb");
});
it("can reset and render children again", async () => {
const user = userEvent.setup();
const onError = vi.fn();
render(<RetryHarness onError={onError} />);
await user.click(screen.getByRole("button", { name: "Retry profile" }));
expect(screen.getByText("Profile loaded")).toBeInTheDocument();
});
});
npm install -D vitest @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom
npm run typecheck
npx vitest run src/components/error-boundary/ErrorBoundary.test.tsx
npm run build
Sichere Prompts Fuer Claude Code
Add React Error Boundaries to this React + TypeScript app.
Constraints:
- Follow the official React Error Boundary model.
- Catch render errors from descendants, but handle event handlers and ordinary async failures separately.
- Implement a shared ErrorBoundary class, user-facing fallback UI, and reportReactError with PII redaction.
- Route-level boundaries must reset on navigation through resetKeys.
- Component-level boundaries should only wrap independent regions such as DashboardChart, MarkdownPreview, and RecommendationPanel.
- Do not log error.stack, query strings, form values, Authorization headers, cookies, or raw API responses without redaction.
- Add Vitest + Testing Library coverage for fallback UI, onError, and retry reset.
- Run npm run typecheck, npx vitest run, and npm run build, then report the results.
Read the existing routing, logging, and CSS conventions first. Keep the diff minimal.
Review-Prompt:
Review this diff only from the Error Boundary perspective.
List issues with boundary placement, async errors that are not caught, PII leakage, missing resetKeys, fallback accessibility, and missing tests.
Do not change code. Return file names and line numbers.
Use Cases Und Typische Fehler
Erster Use Case: SaaS-Dashboard. Umsatzdiagramm, Active-Users-Tabelle, Notification Panel und Third-Party-Embed separat einhuellen. Ein Bug in der Chart-Library darf Settings oder Billing nicht blockieren. Nutze Log-Features wie dashboard.revenue-chart.
Zweiter Use Case: Content Editor. Markdown-Preview, Image-Preview und AI-Summary-Panel sind riskante Bereiche. Body-Editor und Save-Button sind Kernflaechen und sollten erhalten bleiben. Preview kann eine Boundary haben; Save-Fehler gehoeren in den Event Handler.
Dritter Use Case: E-Commerce oder Signup. Kartenablehnung, fehlender Bestand und Validierungsfehler sind keine Boundary-Fehler, sondern erwartete Produktzustaende. Empfehlungsmodule, Kampagnenbanner und Review Widgets koennen isoliert werden.
Vierter Use Case: Admin-Audit-Log. Ein grosser JSON-Viewer kann beim Formatieren werfen. Umhuelle den Viewer, nicht die ganze Seite, damit der Operator Filter aendern, CSV exportieren oder einen anderen User pruefen kann.
Typische Fehler sind: try/catch um JSX fuer ausreichend halten, alle Async-Fehler an die Boundary senden, volle URLs mit Query Strings loggen, Stack Traces in der UI anzeigen, resetten ohne die kaputte Eingabe zu aendern, und so viele kleine Boundaries setzen, dass die Seite aus Fallbacks besteht. Fuer Teams lohnt es sich, Implementierungs- und Review-Prompts als wiederverwendbare Claude-Code-Commands abzulegen. Fuer eine breitere Einfuehrung kann der naechste Schritt zu Claude Code Training und Implementierungssupport fuehren.
Fazit
Eine Error Boundary ist kein universeller Exception Handler. Sie ist eine React-spezifische Grenze fuer Render-Fehler, mit Fallback-UI und sicherem Logging. Wenn du Claude Code nutzt, beschreibe Fangbereich, Route- und Component-Level-Platzierung, Reset-Verhalten, PII-Policy, Tests und Verifikationsbefehle im Prompt.
Im praktischen Dashboard-Test wurde der Diff deutlich leichter reviewbar, nachdem resetKeys und Redaction-Regeln vor dem Code feststanden. Die App fiel nicht mehr komplett aus, wenn ein Widget crashte, und die Logs blieben nutzbar, ohne Nutzerdaten offenzulegen.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude-Code-Permission-Receipt: Scope, Beweis und Rollback festhalten
Permission-Receipt für Claude Code: erlaubte Aktionen, Freigabegrenzen, Prüfbefehle, Rollback und Umsatz-CTA-Prüfung.
Sicheres Agent Harness fur Claude Code und Codex: Rechte, Prufung und Rollback
Ein praktisches Agent Harness fur Claude Code und Codex mit Policy, Plan, Verifikation und Recovery.
Claude Code Subagents: Praxisleitfaden für sichere Agent-Delegation
Claude Code Subagents praktisch nutzen: Artikel- und Codearbeit sicher aufteilen, Prompts einsetzen, Fehler vermeiden.