React-Komponenten können fehlschlagen. Ein API-Call liefert unerwartete Daten, eine Bibliothek wirft eine Exception, ein Entwickler macht einen Tippfehler in einer Template-Expression. Ohne Fehlerbehandlung bedeutet ein einziger Fehler in einer Komponente oft: weiße Seite, keine UI, frustrierte Nutzer. React bietet mit Error Boundaries ein Konzept, das verhindert, dass einzelne Fehler die gesamte Anwendung lahmlegen.
Ein Fehler während des Renderings stoppt React komplett. Die Komponenten-Hierarchie wird nicht weiter aufgebaut, das DOM bleibt unvollständig oder verschwindet ganz.
function ProductList({ products }: { products: Product[] }) {
return (
<div>
{products.map(product => (
<div key={product.id}>
{product.name.toUpperCase()} {/* Was wenn product.name undefined? */}
{product.price.toFixed(2)} {/* Was wenn price keine Zahl? */}
</div>
))}
</div>
);
}
Ein einziges undefined bei product.name
wirft einen TypeError. React kann nicht weiter rendern, die komplette UI
verschwindet. In Production sieht der Nutzer eine leere Seite. In
Development zeigt React einen Error-Overlay – hilfreich für Entwickler,
katastrophal für Endnutzer.
Traditionelles try-catch hilft hier nicht. Es
funktioniert nur für imperativen Code, nicht für deklaratives
Rendering.
// ❌ Funktioniert nicht für Render-Fehler
function ProductList({ products }: { products: Product[] }) {
try {
return (
<div>
{products.map(product => (
<div>{product.name.toUpperCase()}</div>
))}
</div>
);
} catch (error) {
return <div>Error!</div>; // Wird nie erreicht
}
}
Der try-Block umschließt den Return-Statement, aber der
Fehler passiert später – während React das JSX auswertet. Zu diesem
Zeitpunkt ist die Funktion längst verlassen.
Error Boundaries sind spezielle Komponenten, die Fehler in ihrer gesamten Child-Hierarchie abfangen können. Sie sind eines der letzten Use-Cases, wo Klassen-Komponenten unverzichtbar sind – die entsprechenden Lifecycle-Methoden existieren für Function Components nicht.
import React, { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
// Wird während Render-Phase aufgerufen
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Wird während Commit-Phase aufgerufen
console.error('Error caught by boundary:', error, errorInfo);
// Hier könnte man zu Sentry, LogRocket, etc. loggen
// logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div>
<h2>Etwas ist schiefgelaufen</h2>
<button onClick={() => this.setState({ hasError: false, error: null })}>
Erneut versuchen
</button>
</div>
);
}
return this.props.children;
}
}
// Verwendung
function App() {
return (
<ErrorBoundary>
<ProductList products={products} />
</ErrorBoundary>
);
}
Zwei Lifecycle-Methoden machen eine Klasse zur Error Boundary:
getDerivedStateFromError wird synchron
während des Renderings aufgerufen. Sie erhält den Fehler und gibt neuen
State zurück. Keine Side-Effects erlaubt – nur State-Update für
Fallback-UI.
componentDidCatch wird asynchron nach
dem Rendering aufgerufen. Hier gehören Side-Effects hin: Logging,
Analytics, Error-Reporting.
Error Boundaries fangen Fehler in: - Render-Methoden von Child-Komponenten - Lifecycle-Methoden - Konstruktoren der gesamten Hierarchie darunter
Sie fangen NICHT Fehler in: - Event-Handlern - Asynchronem Code (setTimeout, Promises, async/await) - Server-Side Rendering - Der Error Boundary selbst
Eine einzelne Error Boundary um die gesamte App ist zu grob. Ein Fehler in der Sidebar würde den Main Content zerstören. Besser: Mehrere Boundaries auf verschiedenen Ebenen.
function App() {
return (
<ErrorBoundary fallback={<FullPageError />}>
<Header />
<div className="layout">
<ErrorBoundary fallback={<SidebarError />}>
<Sidebar />
</ErrorBoundary>
<main>
<ErrorBoundary fallback={<ContentError />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={
<ErrorBoundary fallback={<ProductListError />}>
<ProductList />
</ErrorBoundary>
} />
</Routes>
</ErrorBoundary>
</main>
</div>
</ErrorBoundary>
);
}
| Boundary-Ebene | Zweck | Fallback |
|---|---|---|
| Root | Letzte Verteidigung | Generische Fehlerseite mit Reload |
| Feature | Isoliert Features | Feature-spezifische Meldung |
| Komponente | Granulare Isolation | Minimaler Placeholder |
Fehler in der Sidebar lassen Main Content intakt. Fehler in einer Produkt-Karte betreffen nicht die Liste.
Error Boundaries fangen keine Event-Handler-Fehler. Diese sind synchron und React hat bereits gerendert, wenn sie ausgeführt werden.
function ProductCard({ product }: { product: Product }) {
// ❌ Error Boundary fängt das nicht
const handleClick = () => {
throw new Error('Click handler error');
};
// ✓ Manuelles try-catch
const handleClickSafe = () => {
try {
// Potentiell fehleranfällige Logik
processProduct(product);
} catch (error) {
console.error('Error processing product:', error);
// Fehler in State speichern für UI-Feedback
setError(error);
}
};
return <button onClick={handleClickSafe}>Process</button>;
}
Für asynchronen Code in Event-Handlern:
function ProductForm() {
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);
const handleSubmit = async (data: FormData) => {
setLoading(true);
setError(null);
try {
await saveProduct(data);
// Success-Logik
} catch (error) {
setError(error instanceof Error ? error : new Error(String(error)));
} finally {
setLoading(false);
}
};
if (error) {
return <div>Error: {error.message}</div>;
}
return <form onSubmit={handleSubmit}>...</form>;
}
Für komplexere Szenarien: Error Boundary mit Context für globales Error-Management.
interface ErrorContextType {
error: Error | null;
resetError: () => void;
setError: (error: Error) => void;
}
const ErrorContext = createContext<ErrorContextType | undefined>(undefined);
export function useError() {
const context = useContext(ErrorContext);
if (!context) {
throw new Error('useError must be used within ErrorBoundaryProvider');
}
return context;
}
interface ErrorBoundaryProviderProps {
children: ReactNode;
fallback?: (error: Error, reset: () => void) => ReactNode;
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
}
export class ErrorBoundaryProvider extends Component
ErrorBoundaryProviderProps,
State
> {
constructor(props: ErrorBoundaryProviderProps) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
this.props.onError?.(error, errorInfo);
}
resetError = () => {
this.setState({ hasError: false, error: null });
};
setError = (error: Error) => {
this.setState({ hasError: true, error });
};
render() {
const { children, fallback } = this.props;
const { hasError, error } = this.state;
const contextValue: ErrorContextType = {
error,
resetError: this.resetError,
setError: this.setError
};
return (
<ErrorContext.Provider value={contextValue}>
{hasError && error
? fallback?.(error, this.resetError) || <DefaultErrorUI error={error} />
: children
}
</ErrorContext.Provider>
);
}
}
// Verwendung in Event-Handlern
function ProductCard({ product }: { product: Product }) {
const { setError } = useError();
const handleDelete = async () => {
try {
await deleteProduct(product.id);
} catch (error) {
// Fehler wird von Error Boundary gefangen und angezeigt
setError(error instanceof Error ? error : new Error(String(error)));
}
};
return <button onClick={handleDelete}>Delete</button>;
}
Suspense ist Reacts Mechanismus für asynchrones Rendering –
hauptsächlich für Code-Splitting mit React.lazy.
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Products = lazy(() => import('./pages/Products'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<ErrorBoundary fallback={<ErrorPage />}>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/products" element={<Products />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</ErrorBoundary>
);
}
Suspense zeigt den Fallback, während die Komponente
lädt. Error Boundary fängt Fehler beim Laden (z.B. Netzwerk-Fehler,
Parse-Fehler).
Kombiniert mit Error Boundaries:
function LazyRoute({
component: Component,
fallback = <LoadingSpinner />,
errorFallback = <RouteError />
}: LazyRouteProps) {
return (
<ErrorBoundary fallback={errorFallback}>
<Suspense fallback={fallback}>
<Component />
</Suspense>
</ErrorBoundary>
);
}
// Verwendung
<Route path="/products" element={
<LazyRoute
component={lazy(() => import('./Products'))}
fallback={<ProductsLoading />}
errorFallback={<ProductsError />}
/>
} />
In Development zeigt React detaillierte Error-Overlays. In Production sind diese deaktiviert – Error Boundaries sind die einzige Fehler-UI.
interface ErrorFallbackProps {
error: Error;
resetError: () => void;
}
function ErrorFallback({ error, resetError }: ErrorFallbackProps) {
const isDev = process.env.NODE_ENV === 'development';
return (
<div className="error-container">
<h2>Etwas ist schiefgelaufen</h2>
{isDev && (
<details>
<summary>Fehlerdetails (nur in Development sichtbar)</summary>
<pre>{error.message}</pre>
<pre>{error.stack}</pre>
</details>
)}
<button onClick={resetError}>
Seite neu laden
</button>
<button onClick={() => window.location.href = '/'}>
Zur Startseite
</button>
</div>
);
}
Error Boundaries sind der ideale Integrationspunkt für Error-Tracking-Services.
import * as Sentry from '@sentry/react';
class ErrorBoundary extends Component<Props, State> {
// ... state und getDerivedStateFromError
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Zu Sentry loggen
Sentry.captureException(error, {
contexts: {
react: {
componentStack: errorInfo.componentStack
}
}
});
// Zusätzliche Custom-Daten
Sentry.setContext('user', {
id: getCurrentUserId(),
route: window.location.pathname
});
}
render() {
// ... Fallback-UI
}
}
Alternativ bietet Sentry eine fertige Error Boundary:
import { ErrorBoundary } from '@sentry/react';
function App() {
return (
<ErrorBoundary
fallback={<ErrorPage />}
showDialog // Zeigt Feedback-Dialog
>
<YourApp />
</ErrorBoundary>
);
}
Error Boundaries zu testen ist trickreich, da React in Tests Fehler anders behandelt. React Testing Library bietet Utilities:
import { render, screen } from '@testing-library/react';
// Komponente die einen Fehler wirft
function BrokenComponent() {
throw new Error('Test error');
}
test('Error Boundary catches errors', () => {
// Console.error mocken, da React Fehler loggt
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
render(
<ErrorBoundary fallback={<div>Error occurred</div>}>
<BrokenComponent />
</ErrorBoundary>
);
expect(screen.getByText('Error occurred')).toBeInTheDocument();
spy.mockRestore();
});
test('Error Boundary resets', () => {
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
const { rerender } = render(
<ErrorBoundary>
<BrokenComponent />
</ErrorBoundary>
);
expect(screen.getByText(/error/i)).toBeInTheDocument();
// Simuliere Reset
rerender(
<ErrorBoundary>
<div>Normal content</div>
</ErrorBoundary>
);
expect(screen.getByText('Normal content')).toBeInTheDocument();
spy.mockRestore();
});
Pattern 1: Reset bei Route-Wechsel
import { useLocation } from 'react-router-dom';
function RouterErrorBoundary({ children }: { children: ReactNode }) {
const location = useLocation();
const [key, setKey] = useState(location.pathname);
useEffect(() => {
// Reset Error Boundary bei Route-Wechsel
setKey(location.pathname);
}, [location.pathname]);
return (
<ErrorBoundary key={key}>
{children}
</ErrorBoundary>
);
}
Pattern 2: Retry-Logik
class RetryErrorBoundary extends Component<Props, State> {
state = { hasError: false, error: null, retryCount: 0 };
static getDerivedStateFromError(error: Error): Partial<State> {
return { hasError: true, error };
}
retry = () => {
this.setState(prev => ({
hasError: false,
error: null,
retryCount: prev.retryCount + 1
}));
};
render() {
if (this.state.hasError) {
const canRetry = this.state.retryCount < 3;
return (
<div>
<p>Error: {this.state.error?.message}</p>
{canRetry ? (
<button onClick={this.retry}>
Erneut versuchen ({3 - this.state.retryCount} übrig)
</button>
) : (
<p>Maximale Versuche erreicht</p>
)}
</div>
);
}
return <>{this.props.children}</>;
}
}
Pattern 3: Feature-Flags für Fehler-UI
function FeatureFlaggedBoundary({ children }: { children: ReactNode }) {
const { isEnabled } = useFeatureFlag('detailed-errors');
return (
<ErrorBoundary
fallback={(error, reset) =>
isEnabled ? (
<DetailedError error={error} reset={reset} />
) : (
<SimpleError reset={reset} />
)
}
>
{children}
</ErrorBoundary>
);
}
Fehler 1: Annahme, Error Boundaries fangen alles
// ❌ Async-Fehler werden NICHT gefangen
function ProductList() {
useEffect(() => {
fetchProducts()
.then(setProducts)
.catch(error => {
// Error Boundary fängt das NICHT
throw error; // Wird nicht propagiert!
});
}, []);
}
// ✓ Eigenes Error-Handling
function ProductList() {
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetchProducts()
.then(setProducts)
.catch(setError); // In State speichern
}, []);
if (error) {
throw error; // Jetzt kann Error Boundary fangen
}
}
Fehler 2: Fehler in der Boundary selbst
// ❌ Wenn Fallback-UI fehlschlägt, gibt's keine Rettung
class ErrorBoundary extends Component {
render() {
if (this.state.hasError) {
// Was wenn FallbackUI selbst fehlschlägt?
return <ComplexFallbackThatMightFail error={this.state.error} />;
}
return this.props.children;
}
}
// ✓ Sichere Fallback-UI
render() {
if (this.state.hasError) {
try {
return this.props.fallback || <SafeMinimalFallback />;
} catch {
// Absolutes Minimum
return <div>Critical Error</div>;
}
}
}
Fehler 3: getDerivedStateFromError mit Side-Effects
// ❌ Side-Effects in getDerivedStateFromError
static getDerivedStateFromError(error: Error) {
logToSentry(error); // ❌ Nicht hier!
return { hasError: true, error };
}
// ✓ Side-Effects in componentDidCatch
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
logToSentry(error, errorInfo); // ✓ Hier!
}
Error Boundaries sind nicht perfekt – sie fangen nicht alles, sind Klassen-basiert in einer Hooks-Welt, und erfordern strategische Platzierung. Aber sie sind essentiell für produktionsreife React-Anwendungen. Kombiniert mit manuellem try-catch für async Code, Suspense für Lazy Loading und Error-Monitoring-Integration bilden sie ein robustes Fehlerbehandlungs-System.