Effektive Error Boundaries in komplexen Anwendungen gestalten
Error-Boundary-Strategien, die Fehler elegant abfangen, sinnvolle Fallbacks liefern, brauchbare Diagnosen melden und den Schaden begrenzen.

Ein einzelner unbehandelter Fehler im React-Komponentenbaum kann die gesamte Anwendung zum Absturz bringen. Die Nutzer sehen einen weißen Bildschirm, Fehlerberichte überfluten dein Monitoring und die Lösung kann so trivial sein wie eine Null-Prüfung in einer Tooltip-Komponente. Error Boundaries verhindern diesen kaskadierenden Ausfall, aber die meisten Implementierungen sind zu simpel — eine einzelne Boundary am App-Root, die eine generische "Something went wrong"-Meldung anzeigt.
Strategische Platzierung von Boundaries verwandelt katastrophale Fehler in lokalisierte Degradierungen, bei denen Nutzer ihre Aufgaben dennoch erledigen können.
Das Problem der Single-Boundary-Architektur
Die meisten Anwendungen wickeln ihren gesamten Baum in eine einzige Error Boundary und nennen es fertig.
// ❌ Single boundary — all-or-nothing failure
function App() {
return (
<ErrorBoundary fallback={<FullPageError />}>
<Header />
<Sidebar />
<MainContent>
<Dashboard />
{/* One broken widget crashes the entire app */}
</MainContent>
<Footer />
</ErrorBoundary>
);
}
// A bug in any component shows FullPageError
// User loses all functionality// ✅ Strategic boundaries — localized failure containment
function App() {
return (
<AppErrorBoundary fallback={<FullPageError />}>
<Header />
<ErrorBoundary
fallback={<SidebarFallback />}
onError={reportError}
>
<Sidebar />
</ErrorBoundary>
<MainContent>
<ErrorBoundary
fallback={<DashboardFallback />}
onError={reportError}
>
<Dashboard />
</ErrorBoundary>
</MainContent>
<Footer />
</AppErrorBoundary>
);
}
// A broken widget shows its own fallback
// Rest of the app works normallyDie äußere Boundary fängt wirklich katastrophale Fehler ab. Innere Boundaries behandeln Fehler auf Komponentenebene und halten den Rest der Anwendung funktionsfähig.
Aufbau einer Production Error Boundary
Die integrierte Error-Boundary-API von React erfordert Klassenkomponenten, aber wir können einen robusten Wrapper bauen, der sich in moderne Patterns integriert.
interface ErrorBoundaryProps {
children: React.ReactNode;
fallback: React.ReactNode | ((error: Error, reset: () => void) => React.ReactNode);
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
resetKeys?: unknown[];
isolationLevel: "page" | "section" | "widget";
}
interface ErrorBoundaryState {
error: Error | null;
errorInfo: React.ErrorInfo | null;
}
class ProductionErrorBoundary extends React.Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
state: ErrorBoundaryState = { error: null, errorInfo: null };
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
return { error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
this.setState({ errorInfo });
this.props.onError?.(error, errorInfo);
// Structured error report
reportBoundaryError({
error: {
message: error.message,
stack: error.stack,
name: error.name,
},
componentStack: errorInfo.componentStack ?? "",
isolationLevel: this.props.isolationLevel,
timestamp: new Date().toISOString(),
url: window.location.href,
});
}
componentDidUpdate(prevProps: ErrorBoundaryProps) {
if (this.state.error && this.props.resetKeys) {
const changed = this.props.resetKeys.some(
(key, i) => key !== prevProps.resetKeys?.[i]
);
if (changed) {
this.setState({ error: null, errorInfo: null });
}
}
}
reset = () => {
this.setState({ error: null, errorInfo: null });
};
render() {
if (this.state.error) {
if (typeof this.props.fallback === "function") {
return this.props.fallback(this.state.error, this.reset);
}
return this.props.fallback;
}
return this.props.children;
}
}Die resetKeys-Prop setzt die Boundary automatisch zurück, wenn bestimmte Werte sich ändern — nützlich zur Wiederherstellung nach Navigation oder Daten-Neuladung. Der reset-Callback ermöglicht einen manuellen Retry aus der Fallback-UI.
Aussagekräftige Fallback-Komponenten
Ein guter Fallback kommuniziert, was fehlgeschlagen ist, und bietet einen Lösungsweg. Generische Fehlermeldungen frustrieren Nutzer, die nicht wissen, ob sie es erneut versuchen, neu laden oder den Support kontaktieren sollen.
interface FallbackProps {
error: Error;
reset: () => void;
context: string;
}
function WidgetFallback({ error, reset, context }: FallbackProps) {
return (
<div role="alert" className="widget-error">
<div className="widget-error-icon">⚠️</div>
<p className="widget-error-message">
Unable to load {context}
</p>
<div className="widget-error-actions">
<button onClick={reset} className="retry-button">
Try again
</button>
<button
onClick={() => {
navigator.clipboard.writeText(
`Error: ${error.message}\nComponent: ${context}`
);
}}
className="copy-error-button"
>
Copy error details
</button>
</div>
</div>
);
}
// Section-level fallback preserves page structure
function SectionFallback({ error, reset, context }: FallbackProps) {
return (
<div role="alert" className="section-error">
<h3>This section encountered an error</h3>
<p>The {context} section couldn't load, but you can still use the rest of the page.</p>
<button onClick={reset}>Reload section</button>
</div>
);
}Fehlerklassifizierung und -Routing
Nicht alle Fehler verdienen dieselbe Behandlung. Transiente Netzwerkfehler sollten automatisch wiederholt werden, während Coding-Bugs die Aufmerksamkeit der Entwickler benötigen.
type ErrorCategory = "transient" | "data" | "render" | "fatal";
function classifyError(error: Error): ErrorCategory {
// Network and timeout errors are likely transient
if (
error.name === "TypeError" &&
error.message.includes("fetch")
) {
return "transient";
}
if (
error.message.includes("timeout") ||
error.message.includes("network")
) {
return "transient";
}
// Data shape errors suggest API contract changes
if (
error instanceof TypeError &&
(error.message.includes("Cannot read properties of undefined") ||
error.message.includes("is not a function"))
) {
return "data";
}
// Render errors from React
if (error.message.includes("render")) {
return "render";
}
return "fatal";
}
interface ErrorRecoveryStrategy {
category: ErrorCategory;
autoRetry: boolean;
maxRetries: number;
retryDelay: number;
notifyUser: boolean;
reportToMonitoring: boolean;
}
const recoveryStrategies: Record<ErrorCategory, ErrorRecoveryStrategy> = {
transient: {
category: "transient",
autoRetry: true,
maxRetries: 3,
retryDelay: 1000,
notifyUser: false,
reportToMonitoring: false,
},
data: {
category: "data",
autoRetry: false,
maxRetries: 0,
retryDelay: 0,
notifyUser: true,
reportToMonitoring: true,
},
render: {
category: "render",
autoRetry: true,
maxRetries: 1,
retryDelay: 0,
notifyUser: true,
reportToMonitoring: true,
},
fatal: {
category: "fatal",
autoRetry: false,
maxRetries: 0,
retryDelay: 0,
notifyUser: true,
reportToMonitoring: true,
},
};Strategie zur Platzierung von Boundaries
Wo du Boundaries platzierst, hängt von deiner Komponentenarchitektur und den Fehlerdomänen ab. Das Ziel ist es, unabhängige Features voneinander zu isolieren.
interface BoundaryPlacement {
level: string;
placement: string;
rationale: string;
}
const placementStrategy: BoundaryPlacement[] = [
{
level: "App shell",
placement: "Around the entire app",
rationale: "Last resort — shows full-page error with reload option",
},
{
level: "Route",
placement: "Around each routed page component",
rationale: "Page failure doesn't break navigation",
},
{
level: "Feature",
placement: "Around independent features (sidebar, chat widget, notifications)",
rationale: "Feature failure doesn't block primary workflow",
},
{
level: "Widget",
placement: "Around individual data-driven widgets (charts, lists, forms)",
rationale: "One broken data source doesn't take out the dashboard",
},
{
level: "Third-party",
placement: "Around any third-party component integration",
rationale: "External code is the most likely source of unexpected errors",
},
];
// Practical example: Dashboard with multiple independent widgets
function Dashboard() {
return (
<div className="dashboard-grid">
<ProductionErrorBoundary
isolationLevel="widget"
fallback={(error, reset) => (
<WidgetFallback error={error} reset={reset} context="Revenue Chart" />
)}
>
<RevenueChart />
</ProductionErrorBoundary>
<ProductionErrorBoundary
isolationLevel="widget"
fallback={(error, reset) => (
<WidgetFallback error={error} reset={reset} context="Active Users" />
)}
>
<ActiveUsersWidget />
</ProductionErrorBoundary>
<ProductionErrorBoundary
isolationLevel="widget"
fallback={(error, reset) => (
<WidgetFallback error={error} reset={reset} context="Recent Orders" />
)}
>
<RecentOrders />
</ProductionErrorBoundary>
</div>
);
}Strukturierte Fehlerberichterstattung
Error-Boundary-Berichte sollten genug Kontext enthalten, damit Entwickler das Problem reproduzieren und beheben können, ohne den Nutzer zu fragen, was passiert ist.
interface BoundaryErrorReport {
error: {
message: string;
stack: string | undefined;
name: string;
};
componentStack: string;
isolationLevel: string;
timestamp: string;
url: string;
userAgent?: string;
sessionId?: string;
}
function reportBoundaryError(report: BoundaryErrorReport): void {
// Deduplicate — don't flood monitoring with repeated errors
const errorKey =
`${report.error.name}:${report.error.message}:${report.isolationLevel}`;
if (recentErrors.has(errorKey)) {
recentErrors.get(errorKey)!.count++;
return;
}
recentErrors.set(errorKey, { report, count: 1, firstSeen: Date.now() });
// Send to monitoring service batch
errorQueue.push(report);
scheduleFlush();
}
const recentErrors = new Map<
string,
{ report: BoundaryErrorReport; count: number; firstSeen: number }
>();
const errorQueue: BoundaryErrorReport[] = [];
function scheduleFlush(): void {
if (errorQueue.length === 1) {
setTimeout(flushErrors, 5000);
}
}
function flushErrors(): void {
if (errorQueue.length === 0) return;
const batch = errorQueue.splice(0);
// Send batch to monitoring endpoint
fetch("/api/errors", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ errors: batch }),
}).catch(() => {
// Monitoring failure shouldn't cause more errors
});
}Wichtige Erkenntnisse
Error Boundaries sind Infrastruktur, keine nachträglichen Gedanken. Platziere sie strategisch auf Route-, Feature- und Widget-Ebene, damit Fehler auf den kleinstmöglichen Blast Radius begrenzt bleiben. Baue Fallback-Komponenten, die kommunizieren, was fehlgeschlagen ist, und umsetzbare Wiederherstellungsoptionen bieten — Retry-Buttons, Kopieren von Fehlerdetails und Navigationsalternativen. Klassifiziere Fehler nach Kategorie, um passende Wiederherstellungsstrategien anzuwenden: automatisches Retry bei transienten Fehlern, Meldung von Datenvertragsverletzungen und Eskalation wirklich fataler Fehler. Wickle Komponenten von Drittanbietern immer in Boundaries ein, da externer Code die unvorhersehbarste Fehlerquelle ist. Das Zeichen einer gut abgegrenzten Anwendung ist, dass Nutzer einen defekten Widget auf einen Blick erkennen und dennoch ihren Workflow ohne Unterbrechung fortsetzen können.


