Zum Inhalt springen

Effektive Error Boundaries in komplexen Anwendungen gestalten

Error-Boundary-Strategien, die Fehler elegant abfangen, sinnvolle Fallbacks liefern, brauchbare Diagnosen melden und den Schaden begrenzen.

5 Min. Lesezeit
Komponentenbaum der Anwendung, der Error Boundaries zeigt, die Fehler an strategischen Punkten mit ansprechenden Fallback-UIs abfangen

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.

tstypescript
// ❌ 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
tstypescript
// ✅ 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 normally

Die ä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.

tstypescript
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.

tstypescript
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.

tstypescript
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.

tstypescript
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.

tstypescript
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.

Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX