Fehler graceful abfangen
Ein nicht abgefangener Fehler in einer React-Komponente zerstört den gesamten Komponentenbaum und zeigt dem Benutzer einen leeren Bildschirm. Error Boundaries sind der Mechanismus, der JavaScript-Fehler im Rendering isoliert, Fallback-UIs zeigt und die Anwendung stabil hält – korrekt implementiert, zuverlässig und mit Monitoring-Integration.
Inhaltsverzeichnis
- 1. Was ohne Error Boundaries passiert
- 2. Wie Error Boundaries funktionieren
- 3. Eigene Error Boundary Klasse implementieren
- 4. Granularität: Wo Error Boundaries platzieren?
- 5. Fallback-UI: Was gute Fehlermeldungen ausmacht
- 6. Reset-Mechanismen: Wie der Benutzer sich erholt
- 7. Fehler-Monitoring: Integration mit Sentry und Co.
- 8. Was Error Boundaries nicht abfangen
- 9. Eigene Implementierung vs. react-error-boundary
- 10. Zusammenfassung
- 11. FAQ
1. Was ohne Error Boundaries passiert
Ohne Error Boundaries hat ein JavaScript-Fehler beim Rendern einer React-Komponente katastrophale Auswirkungen: React unmountet den gesamten Komponentenbaum ab der Wurzel. Der Benutzer sieht einen leeren Bildschirm oder die Browser-Konsole mit einem Fehler – beides ist inakzeptabel für eine Produktionsanwendung. In der Entwicklung zeigt React 18 einen roten Fehleroverlay, der beim Build verschwindet. Ohne Error Boundaries gibt es in der Produktion keinerlei Fallback – nur den leeren Bildschirm.
Das Problem tritt häufiger auf als man denkt: Ein API-Endpunkt gibt unerwartete Daten zurück, eine Komponente versucht, undefined.length zu lesen und wirft einen TypeError. Oder ein npm-Package-Update verändert eine API, die eine Child-Komponente nutzt. Oder der Browser-Cache liefert veraltetes JavaScript, das nicht mehr mit der aktuellen API-Antwort kompatibel ist. Diese Fehler sind in der Praxis schwer vollständig zu verhindern – aber mit Error Boundaries kann man ihren Impact auf den fehlerhaften Bereich der Anwendung begrenzen.
Error Boundaries sind React Class Components mit zwei speziellen Lifecycle-Methoden: static getDerivedStateFromError und componentDidCatch. Das ist einer der wenigen Fälle in modernem React, wo eine Class Component keine Alternative durch Hooks hat – es gibt keinen useErrorBoundary-Hook in React selbst. Für die meisten Projekte ist die Lösung: einmal eine wiederverwendbare Error Boundary Klasse schreiben, oder die Bibliothek react-error-boundary verwenden, die dasselbe tut.
2. Wie Error Boundaries funktionieren
static getDerivedStateFromError(error) wird aufgerufen, wenn ein Fehler im Komponentenbaum unter der Error Boundary geworfen wird. Diese Methode empfängt den Fehler und gibt ein neues State-Objekt zurück, das React für den nächsten Render verwendet. Das typische Muster: return { hasError: true, error }. Im nächsten Render überprüft die render-Methode this.state.hasError und zeigt entweder die Fallback-UI oder die normalen Children.
componentDidCatch(error, errorInfo) wird nach dem Render der Fallback-UI aufgerufen. Es erhält den Fehler und ein ErrorInfo-Objekt mit dem componentStack – einem Stack Trace der React-Komponentenhierarchie bis zum Fehler. Das ist der richtige Ort für Seiteneffekte: Logging, das Senden an Monitoring-Services wie Sentry, das Aktualisieren von Analytics-Events. getDerivedStateFromError ist eine statische Methode und darf keine Seiteneffekte ausführen.
// error-boundary.tsx — Production-ready Error Boundary with TypeScript
import { Component, type ReactNode, type ErrorInfo } from 'react';
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
errorId: string | null;
}
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
onError?: (error: Error, errorInfo: ErrorInfo) => void;
onReset?: () => void;
}
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null, errorId: null };
this.reset = this.reset.bind(this);
}
// Called synchronously after a child throws — update state to show fallback
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
return {
hasError: true,
error,
errorId: `err-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
};
}
// Called after render — good place for side effects like logging
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
const { onError } = this.props;
// Log component stack for debugging
console.error('[ErrorBoundary] Caught error:', error, errorInfo.componentStack);
// Delegate to consumer for monitoring integration
onError?.(error, errorInfo);
}
reset(): void {
this.props.onReset?.();
this.setState({ hasError: false, error: null, errorId: null });
}
render(): ReactNode {
const { hasError, error } = this.state;
const { children, fallback } = this.props;
if (hasError && error) {
// Support both static ReactNode and render function fallbacks
if (typeof fallback === 'function') {
return fallback(error, this.reset);
}
return fallback ?? <DefaultErrorFallback error={error} onReset={this.reset} />;
}
return children;
}
}
4. Granularität: Wo Error Boundaries platzieren?
Die Platzierung von Error Boundaries ist eine der wichtigsten Architekturentscheidungen bei ihrer Implementierung. Eine einzige Error Boundary an der Wurzel der Anwendung schützt vor einem komplett leeren Bildschirm, isoliert Fehler aber nicht – jeder Fehler irgendwo in der Anwendung zeigt dieselbe globale Fallback-UI und macht die gesamte Anwendung unbenutzbar. Das ist besser als ein leerer Bildschirm, aber weit entfernt von der optimalen Nutzererfahrung.
Die bessere Strategie ist granulares Platzieren: Eine Error Boundary auf Routen-Ebene verhindert, dass ein Fehler in einer Route die Navigation zu anderen Routen deaktiviert. Eine Error Boundary um Widget-Komponenten (News-Feed, Produktempfehlungen, Kommentare) stellt sicher, dass ein Fehler in einem Widget nicht die gesamte Seite betrifft. Die Faustregel: Error Boundaries an jedem sinnvollen Isolierungspunkt – überall dort, wo eine Fallback-UI dem Benutzer ermöglicht, den Rest der Anwendung weiter zu nutzen. Das ist typischerweise auf Routen-Ebene und um unabhängige Seitenbereiche herum.
// app-router.tsx — Route-level Error Boundaries with React Router v6
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { ErrorBoundary } from './error-boundary';
// Route-level fallback — allows navigation to other routes
function RouteError({ error, onReset }: { error: Error; onReset: () => void }) {
return (
<div className="min-h-[60vh] flex items-center justify-center">
<div className="text-center max-w-md px-6">
<div className="text-6xl mb-4">⚠</div>
<h2 className="text-xl font-bold text-slate-800 mb-2">Fehler beim Laden dieser Seite</h2>
<p className="text-slate-600 text-sm mb-6">{error.message}</p>
<div className="flex gap-3 justify-center">
<button onClick={onReset} className="btn-primary">Nochmal versuchen</button>
<a href="/" className="btn-secondary">Zur Startseite</a>
</div>
</div>
</div>
);
}
// Widget-level boundary — isolated, page remains functional
function WidgetErrorFallback() {
return (
<div className="rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-700">
Dieser Bereich konnte nicht geladen werden. Die Seite ist weiterhin nutzbar.
</div>
);
}
// Wrap each route with its own Error Boundary
function withRouteBoundary(Component: React.ComponentType) {
return (
<ErrorBoundary
fallback={(error, reset) => <RouteError error={error} onReset={reset} />}
onError={(error, info) => reportToMonitoring(error, { context: 'route', componentStack: info.componentStack })}
>
<Component />
</ErrorBoundary>
);
}
// Wrap independent widgets separately
function Dashboard() {
return (
<div className="grid grid-cols-3 gap-6">
<ErrorBoundary fallback={<WidgetErrorFallback />}>
<RevenueWidget />
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetErrorFallback />}>
<OrdersWidget />
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetErrorFallback />}>
<NewsWidget />
</ErrorBoundary>
</div>
);
}
5. Fallback-UI: Was gute Fehlermeldungen ausmacht
Die Fallback-UI ist das, was der Benutzer sieht, wenn ein Fehler aufgetreten ist. Eine schlechte Fallback-UI ist ein leerer Bereich ohne Erklärung oder eine kryptische technische Fehlermeldung. Eine gute Fallback-UI erklärt in einfachen Worten, was passiert ist, bietet dem Benutzer eine konkrete Aktion an (Seite neu laden, Nochmal versuchen, Support kontaktieren) und gibt wenn möglich eine Referenznummer für den Support, die mit dem Monitoring-Event verknüpft ist.
Für verschiedene Granularitätsebenen braucht man verschiedene Fallback-UIs. Ein globaler Fehler (Wurzel-Error Boundary) rechtfertigt eine vollständige Fehlerseite mit Navigation. Ein Widget-Fehler sollte minimale Aufmerksamkeit erregen und den Benutzer nicht aus dem Lesefluss reißen. Eine Inline-Fehlermeldung mit einem Retry-Button ist bei Widget-Fehlern meistens besser als eine große Fehlerkarte. Die Sprache der Fehlermeldung sollte immer auf den Benutzer ausgerichtet sein, nicht auf den Entwickler.
6. Reset-Mechanismen: Wie der Benutzer sich erholt
Ein oft vergessenes Detail bei Error Boundaries ist der Reset-Mechanismus. Eine Error Boundary, die einen Fehler abfängt und eine Fallback-UI zeigt, bleibt in diesem Fehler-State, bis sie zurückgesetzt wird. Der Benutzer kann die Seite neu laden (vollständiger Reset), aber das ist eine schlechte Erfahrung. Besser: Die Error Boundary bietet einen Retry-Button, der den internen State zurücksetzt und erneut versucht, die Children zu rendern. Wenn der Fehler durch einen vorübergehenden Netzwerkfehler verursacht wurde, löst das das Problem ohne Seitenreload.
React Router bietet einen natürlichen Reset-Trigger: Navigation zu einer anderen Route und zurück setzt Error Boundaries auf Routen-Ebene automatisch zurück, weil die Komponente unmountet und neu gemountet wird. Die Bibliothek react-error-boundary bietet dafür den resetKeys-Prop: Eine Liste von Werten, bei deren Änderung die Error Boundary automatisch zurückgesetzt wird. Das ermöglicht automatisches Retry, wenn sich ein Zustand ändert, der für den Fehler verantwortlich sein könnte.
7. Fehler-Monitoring: Integration mit Sentry und Co.
Error Boundaries ohne Monitoring-Integration sind ein halbes Feature. Man isoliert Fehler vom Benutzer, aber man erfährt nicht, wie oft sie auftreten, welche Benutzer betroffen sind und welche Fehlertypen dominieren. Die Integration mit Monitoring-Services wie Sentry, Datadog oder LogRocket erfolgt in componentDidCatch. Sentry stellt für React die Funktion Sentry.captureException bereit, die auch den React-Component-Stack aus errorInfo.componentStack einschließen kann.
Ein wichtiges Detail: In der Entwicklungsumgebung zeigt React 18 denselben Fehler zweimal – einmal von der Error Boundary abgefangen und einmal vom globalen Error-Handler. Das liegt an Reacts StrictMode-Verhalten: React ruft die Lifecycle-Methoden im Development-Modus zweimal auf, um Nebenwirkungen zu finden. In der Produktion wird componentDidCatch einmalig aufgerufen. Der Monitoring-Code sollte Duplikate im Development durch eine Umgebungsprüfung vermeiden: if (process.env.NODE_ENV === 'production') { captureException(error); } oder die Deduplizierung Sentry selbst überlassen.
// monitoring.ts — Centralized error reporting with context
import * as Sentry from '@sentry/react';
import type { ErrorInfo } from 'react';
interface ErrorContext {
context: 'route' | 'widget' | 'global';
componentStack?: string;
userId?: string;
errorId?: string;
}
export function reportToMonitoring(error: Error, context: ErrorContext): void {
// Skip duplicate reports in development (React StrictMode double-invoke)
if (process.env.NODE_ENV !== 'production') {
console.group('[Monitoring — Dev]');
console.error(error);
console.info('Context:', context);
console.groupEnd();
return;
}
Sentry.withScope((scope) => {
scope.setTag('error.context', context.context);
scope.setTag('error.id', context.errorId ?? 'unknown');
if (context.userId) {
scope.setUser({ id: context.userId });
}
if (context.componentStack) {
scope.setExtra('componentStack', context.componentStack);
}
Sentry.captureException(error);
});
}
// use-error-reporting.ts — Hook for manual error reporting in event handlers
export function useErrorReporting() {
return {
reportError: (error: unknown, context?: Partial<ErrorContext>) => {
const err = error instanceof Error ? error : new Error(String(error));
reportToMonitoring(err, { context: 'widget', ...context });
},
};
}
// Global unhandled rejection handling (outside React tree)
if (typeof window !== 'undefined') {
window.addEventListener('unhandledrejection', (event) => {
const error = event.reason instanceof Error
? event.reason
: new Error(`Unhandled rejection: ${String(event.reason)}`);
reportToMonitoring(error, { context: 'global' });
});
}
8. Was Error Boundaries nicht abfangen
Das Verstehen der Grenzen von Error Boundaries ist genauso wichtig wie das Verstehen ihrer Möglichkeiten. Error Boundaries fangen nur Fehler ab, die während des Renderings auftreten – in der render-Methode, in getDerivedStateFromProps, in Constructors von Child-Komponenten und in React-Lifecycle-Methoden. Sie fangen keine Fehler in Event-Handlern ab, keine Fehler in asynchronem Code (setTimeout, fetch, async/await) und keine Fehler außerhalb des React-Komponentenbaums.
Für Fehler in Event-Handlern verwendet man normales try/catch: const handleClick = () => { try { doSomething(); } catch (err) { setError(err); } }. Für Fehler in asynchronem Code in useEffect gilt dieselbe Regel: try/catch im Effect und State-Update mit dem Fehler. React-Query und SWR haben eigene Fehlerbehandlung für Fetch-Fehler, die unabhängig von Error Boundaries funktioniert. Für das globale Abfangen von nicht behandelten Promise-Rejections ist window.addEventListener('unhandledrejection', ...) der richtige Mechanismus.
9. Eigene Implementierung vs. react-error-boundary
Die Bibliothek react-error-boundary von Brian Vaughn (ehemals React Core Team) ist eine schmale, gut getestete Abstraktion über die native Error Boundary API. Sie bietet einen ErrorBoundary-Component mit FallbackComponent-Prop, einen fallbackRender-Prop für Render-Funktionen, resetKeys für automatischen Reset und den useErrorBoundary-Hook für das manuelle Auslösen von Error Boundaries aus Funktionskomponenten und Event-Handlern.
Der useErrorBoundary-Hook aus react-error-boundary löst ein echtes Problem: Error Boundaries fangen nur Rendering-Fehler, nicht Fehler in Event-Handlern. Mit const { showBoundary } = useErrorBoundary() kann man einen Fehler aus einem Event-Handler oder einem async Effect an die nächste Error Boundary weiterreichen: showBoundary(error). Das ist ein echter Vorteil gegenüber der nativen API. Für neue Projekte empfehle ich react-error-boundary – die ~2 KB zusätzliche Bundle-Größe sind gut investiert für die verbesserte DX und die bewährten Patterns.
| Feature | Eigene Class Component | react-error-boundary |
|---|---|---|
| Bundle-Größe | 0 KB zusätzlich | ~2 KB gzip |
| Reset via resetKeys | Manuell implementieren | Eingebaut |
| Fehler aus Event-Handlern | try/catch + rethrow in render | useErrorBoundary() Hook |
| TypeScript-Support | Manuell typisieren | Vollständig typisiert |
| Wartung | Eigener Code | Aktiv gepflegt |