Warum Fehlerbehandlung mit KI anders ist
Klassische Fehlerbehandlung kennt eine klare Grenze: entweder ein Wert ist da oder er ist null. Eine Datei existiert oder sie existiert nicht. Ein API-Call schlägt fehl mit einem definierten HTTP-Statuscode. Diese Welt ist binär — und das macht sie handhabbar.
Sobald KI ins Spiel kommt, wird die Fehlerklasse komplexer. Ein großes Sprachmodell kann eine syntaktisch korrekte, inhaltlich aber falsche Antwort liefern. Es kann Halluzinieren — und dabei selbstsicher klingen. Es kann Rate-Limit-Fehler produzieren, die sich wie normale API-Timeouts anfühlen, aber eine eigene Retry-Logik brauchen.
Klassische Fehlertypen
- Null-Pointer / undefined
- Netzwerk-Timeout
- Ungültige Eingabe (Validation)
- Datenbankfehler
- HTTP 4xx / 5xx
- Syntax Error im Code
KI-spezifische Fehlertypen
- Halluzinierter Output (inhaltlich falsch)
- Rate Limit / Token Limit überschritten
- Kontext-Fenster voll (context overflow)
- Unparseable JSON aus LLM-Output
- Modell temporär nicht verfügbar (503)
- Semantisch falsches aber syntaktisch gültiges Ergebnis
Claude Code hilft dir, beide Fehlerklassen systematisch zu behandeln — indem es bestehende Patterns erkennt, fehlende Fehlerbehandlung identifiziert, und vollständige Error-Handler generiert.
Claude Code Prompt-Tipp: "Analysiere diese Funktion und ergänze alle fehlenden Error-Handler. Beachte: async/await ohne try/catch, unhandled Promise rejections, und fehlende Input-Validierung."
Error Boundaries in React — Claude Code erklärt und generiert sie
React Error Boundaries sind eine der am häufigsten vergessenen Fehlerbehandlungs-Ebenen in Frontend-Anwendungen. Ohne sie führt ein einzelner Rendering-Fehler in einer tief verschachtelten Komponente zum Absturz der gesamten App — der User sieht einen weißen Bildschirm ohne Hinweis auf das Problem.
Eine Error Boundary ist eine Klassen-Komponente, die den Lifecycle-Hook componentDidCatch implementiert. Sie fängt Fehler in ihrem Komponenten-Baum ab, bevor sie die App zum Absturz bringen.
ErrorBoundary.tsx — generiert mit Claude Code
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, info: ErrorInfo) => void;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error('[ErrorBoundary] Caught:', error, info.componentStack);
this.props.onError?.(error, info);
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? (
<div style={{ padding: '24px', textAlign: 'center' }}>
<h2>Etwas ist schiefgelaufen.</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false, error: null })}>
Erneut versuchen
</button>
</div>
);
}
return this.props.children;
}
}
Der entscheidende Vorteil: du kannst granulare Error Boundaries auf verschiedenen Ebenen einsetzen. Eine Boundary um den gesamten App-Baum schützt vor totalem Absturz. Eine Boundary um ein einzelnes Widget erlaubt, dass der Rest der App weiterläuft.
App.tsx — granulare Error Boundaries
function App() {
return (
<ErrorBoundary fallback={<FullPageError />}>
<Header />
<main>
<ErrorBoundary fallback={<WidgetError message="Dashboard konnte nicht geladen werden" />}>
<RevenueChart />
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetError message="Feed temporär nicht verfügbar" />}>
<ActivityFeed />
</ErrorBoundary>
</main>
</ErrorBoundary>
);
}
Wichtig: Error Boundaries fangen keine Fehler in Event-Handlern, asynchronem Code (setTimeout, Promises), oder Server-Side-Rendering. Für diese Fälle brauchst du zusätzlich try/catch und window.onerror.
Node.js async Error Handling: try/catch, Promise.catch, process.on
In Node.js-Backends gibt es drei Ebenen der Fehlerbehandlung, die zusammen ein vollständiges Netz bilden. Claude Code generiert alle drei — aber wichtiger ist, zu verstehen warum jede Ebene nötig ist.
Ebene 1: Lokales try/catch mit async/await
Die direkteste Methode. Jede async-Funktion die fehlschlagen kann, bekommt ihr eigenes try/catch. Fehler werden spezifisch behandelt — nicht pauschal.
user-service.ts
async function fetchUserData(userId: string): Promise<User> {
try {
const user = await db.users.findById(userId);
if (!user) {
throw new NotFoundError(`Benutzer ${userId} nicht gefunden`);
}
return user;
} catch (err) {
if (err instanceof NotFoundError) {
throw err;
}
logger.error({ err, userId }, 'Datenbankfehler beim Laden des Benutzers');
throw new ServiceError('Benutzerdaten konnten nicht geladen werden');
}
}
Ebene 2: Express/Fastify Error Middleware
Fehler die aus Route-Handlern nach oben propagieren, landen in der Error-Middleware. Diese zentralisiert das Mapping von internen Fehlern auf HTTP-Statuscodes.
error-middleware.ts
import { Request, Response, NextFunction } from 'express';
export function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
const status = err instanceof NotFoundError ? 404
: err instanceof ValidationError ? 422
: err instanceof UnauthorizedError ? 401
: 500;
logger.error({
err,
requestId: req.headers['x-request-id'],
path: req.path,
method: req.method,
}, 'Request fehlgeschlagen');
res.status(status).json({
error: {
message: status < 500 ? err.message : 'Interner Serverfehler',
code: err.constructor.name,
requestId: req.headers['x-request-id'],
}
});
}
Ebene 3: process.on für unhandled Rejections
Der letzte Sicherheitsnetz. Fehler die durch alle anderen Ebenen fallen, landen hier. Im Production-Betrieb sollte dieser Handler den Prozess graceful beenden und einen Neustart triggern.
process-error-handlers.ts
process.on('unhandledRejection', (reason: unknown, promise: Promise<unknown>) => {
logger.fatal({ reason }, 'Unbehandelte Promise-Rejection — Neustart erforderlich');
server.close(() => process.exit(1));
});
process.on('uncaughtException', (err: Error) => {
logger.fatal({ err }, 'Uncaught Exception — Sofort-Neustart');
process.exit(1);
});
process.on('SIGTERM', () => {
logger.info('SIGTERM erhalten — Graceful Shutdown beginnt');
server.close(() => {
db.disconnect();
process.exit(0);
});
});
API-Fehler richtig behandeln: Retry mit Exponential Backoff
Externe APIs — besonders KI-APIs wie die Anthropic-API — können temporär nicht erreichbar sein. Rate Limits, kurze Ausfälle, Netzwerkprobleme: all das rechtfertigt einen automatischen Retry. Das Muster dafür ist Exponential Backoff mit Jitter.
Exponential Backoff bedeutet: nach dem ersten Fehlschlag 1 Sekunde warten, nach dem zweiten 2 Sekunden, nach dem dritten 4 Sekunden. Jitter (zufällige Variation) verhindert dass alle Clients gleichzeitig retrien und den Server überlasten.
retry.ts — Exponential Backoff mit Jitter
interface RetryOptions {
maxAttempts?: number;
baseDelayMs?: number;
maxDelayMs?: number;
retryOn?: (err: Error) => boolean;
}
export async function withRetry<T>(
fn: () => Promise<T>,
options: RetryOptions = {}
): Promise<T> {
const {
maxAttempts = 3,
baseDelayMs = 1000,
maxDelayMs = 30000,
retryOn = (err) => isRetryableError(err),
} = options;
let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (err) {
lastError = err as Error;
if (attempt === maxAttempts || !retryOn(lastError)) {
throw lastError;
}
const exponentialDelay = baseDelayMs * Math.pow(2, attempt - 1);
const jitter = Math.random() * 0.3 * exponentialDelay;
const delay = Math.min(exponentialDelay + jitter, maxDelayMs);
logger.warn(
{ attempt, maxAttempts, delayMs: Math.round(delay), err: lastError },
'Retry nach Fehler'
);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError!;
}
function isRetryableError(err: Error): boolean {
if ((err as any).status === 429) return true;
if ((err as any).status >= 500) return true;
if (err.message.includes('ECONNRESET') || err.message.includes('ETIMEDOUT')) return true;
return false;
}
Claude Code Anwendung: Claude Code nutzt intern einen ähnlichen Retry-Mechanismus für Tool-Calls. Du kannst Claude Code bitten: "Erstelle eine withRetry-Utility für meine Anthropic-API-Calls mit Rate-Limit-Erkennung aus dem Retry-After-Header."
Claude Code für Error Messages: verständliche Fehlermeldungen für User
Der häufigste Fehler bei Fehlermeldungen: Entwickler sehen die interne Error-Message und zeigen sie direkt dem User. "ECONNREFUSED 127.0.0.1:5432" ist kein hilfreicher Fehler für einen Endnutzer.
Claude Code kann dir dabei helfen, eine vollständige Error-Message-Bibliothek zu erstellen — deutsch, klar, mit konkreten Handlungsoptionen. Das Prinzip: interne Fehlercodes werden auf menschenlesbare Nachrichten gemappt.
error-messages.ts — User-facing Fehlermeldungen
export const ERROR_MESSAGES: Record<string, UserErrorMessage> = {
'USER_NOT_FOUND': {
title: 'Konto nicht gefunden',
description: 'Wir konnten kein Konto mit dieser E-Mail-Adresse finden.',
action: 'Bitte prüfe die Eingabe oder registriere ein neues Konto.',
},
'RATE_LIMIT_EXCEEDED': {
title: 'Zu viele Anfragen',
description: 'Du hast in kurzer Zeit zu viele Anfragen gesendet.',
action: 'Bitte warte 60 Sekunden und versuche es erneut.',
retryAfter: 60,
},
'AI_GENERATION_FAILED': {
title: 'KI-Generierung fehlgeschlagen',
description: 'Der KI-Service ist momentan nicht verfügbar.',
action: 'Wir arbeiten daran. Bitte versuche es in wenigen Minuten erneut.',
},
'PAYMENT_DECLINED': {
title: 'Zahlung abgelehnt',
description: 'Deine Zahlung konnte nicht verarbeitet werden.',
action: 'Bitte prüfe deine Zahlungsdaten oder verwende eine andere Karte.',
},
'INTERNAL_ERROR': {
title: 'Etwas ist schiefgelaufen',
description: 'Ein unerwarteter Fehler ist aufgetreten.',
action: 'Bitte lade die Seite neu. Wenn das Problem weiterhin besteht, kontaktiere unseren Support.',
},
};
export function getUserMessage(errorCode: string): UserErrorMessage {
return ERROR_MESSAGES[errorCode] ?? ERROR_MESSAGES['INTERNAL_ERROR'];
}
Logging-Strategie: welche Fehler wie loggen
Nicht jeder Fehler verdient dieselbe Aufmerksamkeit. Eine gut durchdachte Logging-Strategie unterscheidet zwischen Rauschen (das ignoriert werden kann) und Signal (das sofortige Aufmerksamkeit braucht). Für Node.js sind Pino und Winston die etabliertesten Libraries.
🔵
INFO
Normale Ereignisse: Request erhalten, Benutzer eingeloggt, Job gestartet. Kein Handlungsbedarf.
🟡
WARN
Etwas unerwartetes passiert, aber die App läuft weiter: Retry, Fallback aktiv, langsame Query.
🔴
ERROR
Fehler der einen spezifischen Request kaputt macht. Logging + Alert. Muss untersucht werden.
logger.ts — Pino Setup für Production
import pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
transport: process.env.NODE_ENV === 'development'
? { target: 'pino-pretty', options: { colorize: true } }
: undefined,
serializers: {
err: pino.stdSerializers.err,
req: pino.stdSerializers.req,
res: pino.stdSerializers.res,
},
base: {
service: process.env.SERVICE_NAME ?? 'api',
env: process.env.NODE_ENV,
version: process.env.npm_package_version,
},
});
export function createRequestLogger(requestId: string) {
return logger.child({ requestId });
}
Winston bietet mehr Flexibilität bei den Transport-Zielen — besonders wenn du gleichzeitig in eine Datei, nach stdout und an einen externen Service loggen möchtest:
winston-logger.ts — Multi-Transport Setup
import winston from 'winston';
export const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.Console(),
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
maxsize: 10 * 1024 * 1024,
maxFiles: 5,
}),
],
});
Nie loggen: Passwörter, API-Keys, vollständige Kreditkartennummern, personenbezogene Daten ohne Pseudonymisierung. In Deutschland gilt die DSGVO — IP-Adressen und User-IDs in Logs gelten als personenbezogen.
Monitoring: wann Fehler eskalieren — Sentry Integration
Logging sagt dir was passiert ist. Monitoring sagt dir, wenn etwas Unerwartetes passiert. Der Unterschied: Logs musst du aktiv lesen. Monitoring meldet sich bei dir. Sentry ist der De-facto-Standard für Error-Monitoring in JavaScript/TypeScript-Anwendungen.
sentry.ts — Sentry Setup mit Claude Code Patterns
import * as Sentry from '@sentry/node';
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
release: process.env.npm_package_version,
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
beforeSend(event) {
if (event.request?.headers?.authorization) {
event.request.headers.authorization = '[Filtered]';
}
return event;
},
});
export function captureError(
err: Error,
context: Record<string, unknown> = {}
) {
Sentry.withScope((scope) => {
scope.setExtras(context);
if (err instanceof NotFoundError || err instanceof ValidationError) {
scope.setLevel('warning');
}
Sentry.captureException(err);
});
}
Claude Code für Sentry-Integration: Du kannst Claude Code beauftragen: "Ergänze Sentry.captureException in allen catch-Blöcken meiner Express-Routes — aber nur für 5xx-Fehler, nicht für 4xx. Filtere außerdem Authorization-Header aus den Breadcrumbs."
Fehlerbehandlung mit Claude Code: die Stärken im Überblick
Claude Code ist besonders stark darin, bestehenden Code zu analysieren und fehlende Fehlerbehandlung zu ergänzen. Ein typischer Workflow:
- Analyse: "Welche Funktionen in diesem Service haben kein Error Handling?" — Claude Code scannt den gesamten Code und listet alle riskanten Stellen auf.
- Generierung: "Ergänze try/catch in allen async-Funktionen die Datenbankaufrufe machen." — Konsistente Implementierung über hunderte von Zeilen in Sekunden.
- Custom Errors: "Erstelle eine Error-Hierarchie mit NotFoundError, ValidationError, UnauthorizedError und ServiceError — alle mit HTTP-Status-Mapping." — Vollständige Fehler-Klassen inklusive Serialisierung.
- Tests: "Schreibe Jest-Tests für die Error-Boundary und die withRetry-Utility — inklusive Edge Cases wie maxAttempts = 1 und nicht-retriefähige Fehler." — Test-Coverage für Fehlerpfade die manuell schwer zu testen sind.
Sollte ich Error Handling manuell schreiben oder komplett an Claude Code delegieren?
Keins von beiden ist optimal allein. Verstehe die Patterns — was ist retry-fähig, wann ist ein Fehler wirklich fatal, wie sehen gute User-Messages aus. Dann nutze Claude Code für die repetitive Implementierung. Überprüfe jeden generierten Handler ob er die richtigen Fehlerklassen behandelt.
Pino oder Winston?
Pino ist schneller (bis zu 5x laut Benchmark), hat weniger Overhead, und ist ideal für High-Throughput-APIs. Winston ist flexibler bei Transporten und hat mehr Integrations-Plugins. Für neue Projekte: Pino. Für Legacy-Projekte die bereits Winston nutzen: nicht migrieren ohne konkreten Anlass.
Wann ist ein uncaughtException ein echter P0?
Immer. Ein uncaughtException bedeutet, dass ein synchroner Fehler nicht durch try/catch abgefangen wurde. Der Zustand der Anwendung ist nach einem uncaughtException unbekannt — Memory-Leaks, korrupte Daten, inkonsistente State-Maschinen sind möglich. Sofortige Alarmierung + Neustart ist die einzig korrekte Reaktion.
Robustes Error Handling mit Claude Code lernen
Im Claude Code Mastery Kurs behandeln wir Error-Handling-Patterns als eigenes Modul: von Error Boundaries bis Sentry-Integration — mit echten Production-Beispielen aus unserem Unternehmen.
14 Tage kostenlos testen →