Claude Code Fehlerbehandlung: Error Handling Patterns für Production 2026

Fehler sind unvermeidlich — schlechte Fehlerbehandlung nicht. Claude Code generiert robuste Error-Handling-Patterns für React, Node.js und APIs. Was du wissen musst, um deine Anwendung production-ready zu machen.

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) { // Fehler an Monitoring-Service melden (z.B. Sentry) 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> {/* Dashboard-Widget: Fehler hier killen nicht die ganze App */} <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) { // Bekannter Fehler — an Aufrufer weitergeben throw err; } // Unbekannter Fehler — loggen und in generischen Fehler umwandeln 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
// Unbehandelte Promise-Rejections process.on('unhandledRejection', (reason: unknown, promise: Promise<unknown>) => { logger.fatal({ reason }, 'Unbehandelte Promise-Rejection — Neustart erforderlich'); // Graceful shutdown: laufende Requests abschließen, dann beenden server.close(() => process.exit(1)); }); // Synchrone unkorrekte Ausnahmen process.on('uncaughtException', (err: Error) => { logger.fatal({ err }, 'Uncaught Exception — Sofort-Neustart'); // Bei uncaughtException ist der Zustand unbekannt → sofort beenden process.exit(1); }); // SIGTERM (z.B. Docker-Stop, Kubernetes-Eviction) 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; } // Exponential Backoff: 1s → 2s → 4s → ... (mit Jitter) 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 { // Rate Limit: immer retrien if ((err as any).status === 429) return true; // Server-Fehler: retrien if ((err as any).status >= 500) return true; // Netzwerkfehler: retrien if (err.message.includes('ECONNRESET') || err.message.includes('ETIMEDOUT')) return true; // 4xx-Fehler (außer 429): nicht retrien — der Client-Request ist falsch 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', // Strukturiertes JSON in Production, lesbares Format in Development 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, }, }); // Request-Context via AsyncLocalStorage (Korrelations-ID für alle Logs eines Requests) 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: [ // Alle Logs in stdout (für Container-Logging) new winston.transports.Console(), // ERROR-Level: zusätzlich in separate Datei new winston.transports.File({ filename: 'logs/error.log', level: 'error', maxsize: 10 * 1024 * 1024, // 10MB rotation 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, // Nur 10% der normalen Traces → Performance-Monitoring ohne Kosten-Explosion tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0, // Sensitive Daten vor dem Senden entfernen beforeSend(event) { if (event.request?.headers?.authorization) { event.request.headers.authorization = '[Filtered]'; } return event; }, }); // Custom Error-Reporting mit Kontext export function captureError( err: Error, context: Record<string, unknown> = {} ) { Sentry.withScope((scope) => { scope.setExtras(context); // Fehlerklassifizierung: bekannte Fehler mit niedrigerer Priorität if (err instanceof NotFoundError || err instanceof ValidationError) { scope.setLevel('warning'); } Sentry.captureException(err); }); } // Wann eskalieren? Alert-Schwellen definieren: // - error-rate > 1% in 5min → PagerDuty // - neue Error-Klasse zum ersten Mal → sofort // - uncaughtException → sofort P0 // - 5xx-Rate > 5% → sofort P1
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:

  1. Analyse: "Welche Funktionen in diesem Service haben kein Error Handling?" — Claude Code scannt den gesamten Code und listet alle riskanten Stellen auf.
  2. Generierung: "Ergänze try/catch in allen async-Funktionen die Datenbankaufrufe machen." — Konsistente Implementierung über hunderte von Zeilen in Sekunden.
  3. Custom Errors: "Erstelle eine Error-Hierarchie mit NotFoundError, ValidationError, UnauthorizedError und ServiceError — alle mit HTTP-Status-Mapping." — Vollständige Fehler-Klassen inklusive Serialisierung.
  4. 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 →