generische Fallbacks statt any im Rendering Fehlerfall
Das React Error Boundary Interface arbeitet intern mit dem Typ unknown fuer gefangene Fehler, was Fallback Komponenten oft zwingt, auf any oder unsichere Casts auszuweichen. Mit generischen Typparametern und Discriminated Error Types lassen sich Error Boundaries bauen, die dem Aufrufer konkrete, typisierte Fehlerinformationen liefern.
Inhaltsverzeichnis
- 1. Was Error Boundaries loesen und wo TypeScript ansetzt
- 2. Die Grenzen des Error Boundary Interface typsicher erweitern
- 3. Einen generischen Error Boundary Component Typ bauen
- 4. Fallback Props typsicher an den Fehlertyp koppeln
- 5. Error Boundaries mit Discriminated Error Types kombinieren
- 6. Async Fehler und Error Boundaries: was TypeScript nicht abfaengt
- 7. Error Boundaries testen mit typsicheren Test Utilities
- 8. Typische Fehler beim Einsatz von Error Boundaries
- 9. Error Boundary Strategien im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was Error Boundaries loesen und wo TypeScript ansetzt
Eine Error Boundary ist eine React Komponente, die Fehler waehrend des Renderings, in Lifecycle Methoden und in Konstruktoren ihrer Kindkomponenten abfaengt, statt den kompletten Komponentenbaum abstuerzen zu lassen. Ohne Error Boundary fuehrt ein einzelner unbehandelter Fehler in einer tief verschachtelten Komponente dazu, dass React den gesamten sichtbaren Baum entfernt, ein Verhalten, das seit React 16 so vorgesehen ist, um inkonsistente UI Zustaende zu vermeiden.
Das React Interface fuer Error Boundaries stammt aus einer Zeit vor breiter TypeScript Adoption und ist entsprechend locker typisiert: Die Methode componentDidCatch(error: Error, errorInfo: ErrorInfo) liefert zwar einen konkreten Error Typ, aber alles, was danach in der Fallback Anzeige passiert, bleibt der eigenen Implementierung ueberlassen. Genau hier setzt Typsicherheit an: statt den gefangenen Fehler als unknown oder any durch die Fallback Komponente zu reichen, lassen sich generische Typparameter nutzen, um konkrete Fehlerinformationen typsicher weiterzugeben.
Der Mehrwert typsicherer Error Boundaries zeigt sich vor allem in groesseren Anwendungen mit mehreren, unterschiedlich spezialisierten Boundaries: eine fuer Datenabruf Fehler, eine fuer Rendering Fehler in Drittanbieter Widgets, eine fuer Zahlungs Komponenten. Jede dieser Boundaries kann von einem generischen Basistyp erben, der den erwarteten Fehlertyp als Typparameter entgegennimmt, sodass die Fallback Komponente exakt weiss, welche Felder im Fehlerobjekt verfuegbar sind.
2. Die Grenzen des Error Boundary Interface typsicher erweitern
React selbst bietet keinen Hook Ersatz fuer Error Boundaries, sie muessen weiterhin als Klassenkomponente implementiert werden, weil componentDidCatch und getDerivedStateFromError nur in der Klassen API existieren. Fuer TypeScript bedeutet das: die Basisklasse React.Component<P, S> wird um einen State erweitert, der den gefangenen Fehler haelt, typischerweise als { hasError: boolean; error: Error | null }. Diese minimale Signatur reicht fuer eine generische Fallback Anzeige, verliert aber jede Struktur, sobald mehrere Fehlerarten unterschiedlich behandelt werden sollen.
Ein sinnvoller erster Schritt ist, den State Typ als generischen Parameter zu formulieren, sodass eine konkrete Error Boundary Unterklasse einen praeziseren Fehlertyp als Error deklarieren kann, etwa eine eigene DataFetchError Klasse mit zusaetzlichen Feldern wie statusCode. Wichtig ist dabei, dass getDerivedStateFromError weiterhin unknown als Eingabetyp erhaelt, weil JavaScript technisch erlaubt, beliebige Werte zu werfen, nicht nur Error Instanzen. Die Typverfeinerung muss also innerhalb der Methode erfolgen, nicht in der Signatur.
Diese Einschraenkung ist keine TypeScript Schwaeche, sondern spiegelt die tatsaechliche JavaScript Semantik wider: throw "a string" ist gueltiger Code, auch wenn er selten sinnvoll ist. Eine robuste Error Boundary Implementierung prueft deshalb zur Laufzeit, ob der gefangene Wert tatsaechlich eine Error Instanz oder eine eigene, erwartete Fehlerklasse ist, bevor sie auf spezifische Felder zugreift.
import { Component, type ErrorInfo, type ReactNode } from "react";
// Base state shape shared by all boundaries in this app
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
interface ErrorBoundaryProps {
children: ReactNode;
fallback: (error: Error) => ReactNode;
}
class BaseErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
state: ErrorBoundaryState = { hasError: false, error: null };
static getDerivedStateFromError(caught: unknown): ErrorBoundaryState {
// JavaScript allows throwing anything, so this always starts as unknown
const error = caught instanceof Error ? caught : new Error(String(caught));
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
console.error("Boundary caught:", error, errorInfo.componentStack);
}
render(): ReactNode {
if (this.state.hasError && this.state.error) {
return this.props.fallback(this.state.error);
}
return this.props.children;
}
}
3. Einen generischen Error Boundary Component Typ bauen
Der naechste Schritt ist, die Error Boundary selbst generisch ueber den erwarteten Fehlertyp zu machen, statt fest auf Error zu setzen. Dazu wird ein generischer Typparameter E extends Error eingefuehrt, der sowohl im State als auch in den Fallback Props verwendet wird. Der Aufrufer instanziiert die Error Boundary dann mit einem konkreten Fehlertyp, etwa ErrorBoundary<ApiError>, und die Fallback Funktion bekommt direkt den praezisen Typ, ohne manuellen Cast.
Diese Generalisierung bringt insbesondere in Codebasen mit mehreren spezialisierten Error Boundaries einen spuerbaren Vorteil: eine Boundary um eine Zahlungs Komponente kann strikt auf PaymentError typisiert werden, waehrend eine Boundary um ein Drittanbieter Widget generisch bei Error bleibt, weil dort keine spezifischeren Informationen erwartbar sind. Der Compiler verhindert dabei, dass eine falsch typisierte Fallback Funktion versehentlich an die falsche Boundary gebunden wird.
Ein Detail, das leicht uebersehen wird: TypeScript Generics an Klassenkomponenten funktionieren technisch, aber die Typinferenz bei JSX Verwendung ist manchmal eingeschraenkt, insbesondere in aelteren TypeScript Versionen. Ab TypeScript 4.4 funktioniert die Inferenz bei generischen Komponenten in JSX zuverlaessig genug, um auf explizite Typparameter in den meisten Faellen verzichten zu koennen, solange die fallback Prop selbst korrekt typisiert ist.
import { Component, type ReactNode } from "react";
interface GenericErrorBoundaryProps<E extends Error> {
children: ReactNode;
fallback: (error: E) => ReactNode;
// Type guard lets each boundary decide which errors it actually handles
isExpectedError: (caught: unknown) => caught is E;
}
interface GenericErrorBoundaryState<E extends Error> {
error: E | null;
}
class ErrorBoundary<E extends Error> extends Component<
GenericErrorBoundaryProps<E>,
GenericErrorBoundaryState<E>
> {
state: GenericErrorBoundaryState<E> = { error: null };
componentDidCatch(caught: unknown): void {
if (this.props.isExpectedError(caught)) {
this.setState({ error: caught });
} else {
throw caught; // let a parent boundary or React itself handle it
}
}
render(): ReactNode {
if (this.state.error) {
return this.props.fallback(this.state.error);
}
return this.props.children;
}
}
class PaymentError extends Error {
constructor(message: string, readonly declineCode: string) {
super(message);
this.name = "PaymentError";
}
}
function isPaymentError(caught: unknown): caught is PaymentError {
return caught instanceof PaymentError;
}
4. Fallback Props typsicher an den Fehlertyp koppeln
Sobald die Error Boundary selbst generisch ist, lohnt sich derselbe Ansatz auch fuer die Fallback Komponente. Statt einer Funktion, die nur einen Fehler entgegennimmt, kann eine dedizierte Fallback Komponente Props erhalten, die exakt auf den Fehlertyp zugeschnitten sind, etwa retryable: boolean bei einem Netzwerkfehler oder declineCode: string bei einem Zahlungsfehler. Diese Kopplung macht die Fallback UI selbst testbar und wiederverwendbar, unabhaengig von der konkreten Error Boundary, die sie aufruft.
Ein bewaehrtes Muster ist, die Fallback Props als eigenen generischen Typ zu definieren, der aus dem Fehlertyp abgeleitet wird, statt sie manuell zu duplizieren. Mit einem Mapped Type laesst sich aus PaymentError automatisch ein PaymentErrorFallbackProps Typ ableiten, der die relevanten Felder des Fehlers plus zusaetzliche UI spezifische Props wie onRetry: () => void kombiniert. Diese Ableitung stellt sicher, dass eine Aenderung am Fehlertyp automatisch im Fallback Props Typ sichtbar wird, ohne zwei Stellen manuell synchron zu halten.
In der Praxis zeigt sich der Wert dieser Kopplung besonders bei Code Reviews: ein Reviewer sieht sofort am Typ der Fallback Komponente, welche Informationen im Fehlerfall tatsaechlich verfuegbar sind, ohne die Error Boundary Implementierung selbst lesen zu muessen. Das reduziert die kognitive Last erheblich gegenueber einer generischen fallback: ReactNode Prop, die keinerlei Information ueber den Fehlerkontext traegt.
// Fallback props derived directly from the error type
type FallbackProps<E extends Error> = {
error: E;
onRetry: () => void;
};
function PaymentErrorFallback({ error, onRetry }: FallbackProps<PaymentError>) {
return (
<div role="alert">
<p>Payment failed: {error.message}</p>
<p>Decline code: {error.declineCode}</p>
<button onClick={onRetry}>Try again</button>
</div>
);
}
// The boundary passes a bound retry handler into the fallback
function CheckoutSection() {
const [attempt, setAttempt] = useState(0);
return (
<ErrorBoundary<PaymentError>
isExpectedError={isPaymentError}
fallback={(error) => (
<PaymentErrorFallback error={error} onRetry={() => setAttempt((n) => n + 1)} />
)}
>
<PaymentForm key={attempt} />
</ErrorBoundary>
);
}
function useState<T>(initial: T): [T, (updater: (prev: T) => T) => void] {
// placeholder signature for illustration only
return [initial, () => {}];
}
function PaymentForm() {
return null;
}
5. Error Boundaries mit Discriminated Error Types kombinieren
Eine einzelne Error Boundary, die mehrere unterschiedliche Fehlerarten abfaengt, profitiert stark von einer Discriminated Union als Fehlertyp, statt mehrerer Klassenhierarchien. Anstatt instanceof Pruefungen fuer jede Fehlerart einzeln durchzufuehren, wandelt eine Error Boundary den gefangenen Wert in eine typisierte Union um und delegiert die Darstellung an eine switch Anweisung in der Fallback Komponente, die dank Exhaustiveness Checking keine Variante vergisst.
Dieses Muster verbindet die beiden in verwandten Artikeln behandelten Konzepte: Die Error Boundary fungiert als Uebersetzungsschicht zwischen der rohen, ungetypten JavaScript Exception und einer sauberen, im Rest der Anwendung verwendeten Discriminated Union. Die Fallback Komponente selbst muss dann nichts mehr ueber React Interna wissen, sie konsumiert einfach eine Union wie jede andere Komponente auch.
Ein praktischer Vorteil: Wird spaeter eine neue Fehlerart eingefuehrt, meldet der never basierte Exhaustiveness Check in der Fallback Komponente sofort, dass ein neuer Fall fehlt, genau wie bei jeder anderen Discriminated Union Verarbeitung auch. Die Error Boundary selbst muss dafuer nicht angefasst werden, solange die Uebersetzungsfunktion vom gefangenen Wert in die Union alle Faelle abdeckt.
type RenderError =
| { kind: "payment"; error: PaymentError }
| { kind: "network"; statusCode: number }
| { kind: "unknown"; raw: unknown };
function classifyCaughtValue(caught: unknown): RenderError {
if (isPaymentError(caught)) {
return { kind: "payment", error: caught };
}
if (caught instanceof Error && "statusCode" in caught) {
return { kind: "network", statusCode: (caught as { statusCode: number }).statusCode };
}
return { kind: "unknown", raw: caught };
}
function renderFallback(classified: RenderError): ReactNode {
switch (classified.kind) {
case "payment":
return <PaymentErrorFallback error={classified.error} onRetry={() => {}} />;
case "network":
return <p>Network error, status {classified.statusCode}</p>;
case "unknown":
return <p>Unexpected error occurred</p>;
}
}
type ReactNode = unknown;
6. Async Fehler und Error Boundaries: was TypeScript nicht abfaengt
Ein haeufiges Missverstaendnis, unabhaengig von TypeScript, ist die Annahme, dass eine Error Boundary alle Fehler in ihrem Kindbaum abfaengt. React Error Boundaries fangen ausschliesslich Fehler waehrend des Renderings ab, nicht in Event Handlern, nicht in setTimeout Callbacks und nicht in asynchronen Funktionen wie einem fetch Aufruf innerhalb eines useEffect. Dieses Verhalten ist reine React Laufzeit Semantik und wird von TypeScript weder erzwungen noch signalisiert, weil der Typ einer Komponente keine Information darueber traegt, in welchem Ausfuehrungskontext ein Fehler auftritt.
Fuer asynchrone Fehler bleibt die Kombination aus try/catch innerhalb des useEffect und lokalem Error State die richtige Loesung, oft in Verbindung mit dem Result Type Pattern aus einem verwandten Artikel. Ein praktisches Muster ist, asynchrone Fehler manuell in den React Render Zyklus zu re throw, indem der Fehler in einem State gespeichert und dann synchron waehrend des naechsten Renders geworfen wird. Dieser Trick nutzt aus, dass ein waehrend des Renderings geworfener Fehler von der umschliessenden Error Boundary tatsaechlich gefangen wird.
Diese Grenze sauber zu dokumentieren ist wichtig, weil ein falsches Vertrauen in Error Boundaries fuer asynchronen Code zu unbehandelten Promise Rejections fuehrt, die in der Konsole als Warnung erscheinen, aber die UI nicht in einen definierten Fehlerzustand versetzen. Ein zentraler globaler Handler fuer window.addEventListener("unhandledrejection", ...) kann als zusaetzliches Sicherheitsnetz dienen, ersetzt aber keine gezielte, komponentennahe Fehlerbehandlung.
// Re-throwing an async error during render so an Error Boundary can catch it
function useAsyncErrorRethrow(): (error: unknown) => void {
const [, setState] = useState<unknown>();
return (error: unknown) => {
setState(() => {
throw error; // thrown synchronously during the next render
});
};
}
function useState<T>(initial?: T): [T | undefined, (updater: () => never) => void] {
return [initial, () => {}];
}
7. Error Boundaries testen mit typsicheren Test Utilities
Das Testen einer Error Boundary erfordert, absichtlich einen Fehler in einer Kindkomponente zu werfen, was in einer typisierten Testumgebung eine kleine Hilfskomponente rechtfertigt, die generisch genug ist, um mit unterschiedlichen Fehlertypen wiederverwendet zu werden. Eine typsichere Testkomponente ThrowError<E extends Error> nimmt eine Fehlerinstanz als Prop entgegen und wirft sie im Rendering, sodass sich jede beliebige Error Boundary gezielt mit dem passenden Fehlertyp testen laesst.
Ein wichtiger Aspekt ist, React Testing Library Assertions gegen die tatsaechlich gerenderte Fallback UI zu richten, nicht gegen interne Implementierungsdetails der Error Boundary. Der Test prueft also, ob die erwartete Fallback Nachricht im DOM erscheint, nicht ob ein internes State Feld korrekt gesetzt wurde. Diese Herangehensweise bleibt stabil, auch wenn die interne Implementierung der Error Boundary spaeter refactored wird.
Ein zweiter wichtiger Testfall ist die Exhaustiveness Pruefung selbst: Ein Unit Test, der eine neue, noch nicht behandelte Variante der Discriminated Union simuliert, sollte zur Kompilierzeit fehlschlagen, nicht erst zur Laufzeit. Da TypeScript diese Pruefung bereits beim Kompilieren durchfuehrt, reicht ein einfacher Build oder Type Check Schritt in der CI Pipeline aus, um fehlende Faelle zuverlaessig zu erkennen, noch bevor ein Test overhaupt ausgefuehrt wird.
// Generic helper component for testing any Error Boundary
function ThrowError<E extends Error>({ error }: { error: E }): never {
throw error;
}
// Example test with React Testing Library (Jest/Vitest syntax)
test("renders payment fallback with decline code", () => {
render(
<ErrorBoundary<PaymentError>
isExpectedError={isPaymentError}
fallback={(error) => (
<PaymentErrorFallback error={error} onRetry={() => {}} />
)}
>
<ThrowError error={new PaymentError("Card declined", "insufficient_funds")} />
</ErrorBoundary>
);
expect(screen.getByText(/insufficient_funds/)).toBeInTheDocument();
});
function render(_node: unknown): void {}
const screen = { getByText: (_matcher: unknown) => ({ toBeInTheDocument: () => {} }) };
function test(_name: string, _fn: () => void): void {}
function expect(_value: unknown) {
return { toBeInTheDocument: () => {} };
}
8. Typische Fehler beim Einsatz von Error Boundaries
Der haeufigste Fehler ist, den gefangenen Fehler als any zu typisieren, um schnell voranzukommen, und diese lockere Typisierung durch die gesamte Fallback Kette zu propagieren. Sobald any einmal im State einer Error Boundary auftaucht, verliert jede nachgelagerte Komponente ihre Typsicherheit, weil TypeScript any klaglos durch beliebig viele Funktionsaufrufe durchreicht, ohne jemals einen Fehler zu melden.
// WRONG: any defeats the entire purpose of a typed error boundary
interface LooseState {
error: any; // silently propagates through every consumer
}
// WRONG: assuming Error Boundaries catch async errors
useEffect(() => {
fetchData().catch((error) => {
throw error; // does NOT get caught by a surrounding Error Boundary
});
}, []);
// RIGHT: convert the async failure into local state, then rethrow during render
useEffect(() => {
fetchData().catch((error: unknown) => {
setAsyncError(error); // triggers a synchronous throw on next render
});
}, []);
function useEffect(_fn: () => void, _deps: unknown[]): void {}
function fetchData(): Promise<void> {
return Promise.resolve();
}
function setAsyncError(_error: unknown): void {}
Ein zweiter verbreiteter Fehler ist, eine einzige, globale Error Boundary um die gesamte Anwendung zu legen und keine feingranularen Boundaries um einzelne Abschnitte zu platzieren. Das fuehrt dazu, dass ein Fehler in einem unwichtigen Widget die komplette Seite durch eine generische Fallback Meldung ersetzt, statt nur den betroffenen Abschnitt zu isolieren. Mehrere, gezielt platzierte Error Boundaries mit spezifischen Fehlertypen erlauben eine deutlich bessere Fehlerisolation.
9. Error Boundary Strategien im Vergleich
Die folgende Tabelle vergleicht unterschiedliche Strategien zur Typisierung von Error Boundaries anhand praxisrelevanter Kriterien.
| Kriterium | any im State | Fixer Error Typ | Generische Boundary mit Union |
|---|---|---|---|
| Typsicherheit im Fallback | Keine | Nur fuer einen Fehlertyp | Vollstaendig, pro Variante |
| Exhaustiveness Checking | Nicht moeglich | Nicht relevant | Ja, mit switch und never |
| Wiederverwendbarkeit | Hoch, aber unsicher | Gering | Hoch und sicher |
| Implementierungsaufwand | Gering | Gering | Mittel |
| Geeignet fuer async Fehler | Nein | Nein | Nur mit Re Throw Pattern |
Fuer kleine Anwendungen mit wenigen Fehlerquellen reicht ein fixer Error Typ meist aus. Sobald mehrere, klar unterscheidbare Fehlerarten in unterschiedlichen Bereichen der Anwendung auftreten, zahlt sich die generische Error Boundary mit Discriminated Union aus, weil sie Exhaustiveness Checking und Wiederverwendbarkeit gleichzeitig bietet, ohne Typsicherheit an irgendeiner Stelle der Kette zu opfern.
Mironsoft
React und TypeScript Architektur fuer Hyvae und Node.js Frontends
Error Boundaries, die wirklich helfen statt nur zu verstecken?
Wir bauen generische, typsichere Error Boundaries mit gezielten Fallback Komponenten und richten die passende Teststrategie fuer euer React oder Hyvae Frontend ein.
Boundary Design
Generische Error Boundaries mit Discriminated Error Types entwerfen
Async Fehlerbehandlung
Re Throw Pattern und globale Handler fuer unhandled Rejections
Testing Setup
Typsichere Testkomponenten fuer Error Boundaries aufbauen
10. Zusammenfassung
Typsichere Error Boundaries ersetzen die lockere Error beziehungsweise any Typisierung des klassischen React Interface durch generische Typparameter, die den erwarteten Fehlertyp explizit machen. Ein Typ Guard entscheidet, welche Fehlerarten eine konkrete Error Boundary tatsaechlich behandelt, waehrend unbekannte Fehler an eine uebergeordnete Boundary weitergereicht werden. Fallback Props, die direkt aus dem Fehlertyp abgeleitet werden, machen die Fallback UI selbst testbar und unabhaengig von der internen Boundary Implementierung.
Wichtig bleibt die klare Grenze: Error Boundaries fangen ausschliesslich Fehler waehrend des Renderings ab, niemals in Event Handlern oder asynchronen Callbacks. Fuer asynchrone Fehler bleibt ein Re Throw Pattern in Kombination mit lokalem State die richtige Loesung. Wer diese Grenze kennt und Discriminated Unions fuer mehrere Fehlerarten in einer einzigen Error Boundary nutzt, bekommt Exhaustiveness Checking und testbare Fallback Komponenten gleichzeitig.
Typsichere Error Boundaries, das Wichtigste auf einen Blick
Generischer Typparameter
E extends Error macht den erwarteten Fehlertyp explizit statt any oder unknown durchzureichen.
Fallback Props
Direkt aus dem Fehlertyp abgeleitet, testbar und unabhaengig von der Boundary Implementierung.
Async Grenze
Error Boundaries fangen nur Rendering Fehler, async Fehler brauchen ein Re Throw Pattern.
Discriminated Unions
Mehrere Fehlerarten in einer Boundary ueber eine Union mit Exhaustiveness Checking abbilden.