Challenge 7.4: Error Handling
Was passiert in Deiner App, wenn der LLM-Provider mitten im Stream einen Fehler wirft? Der User hat schon die halbe Antwort gelesen — und dann? Weisse Seite? Abbruch ohne Erklärung? Oder eine hilfreiche Fehlermeldung?
OVERVIEW
Abschnitt betitelt „OVERVIEW“Fehler passieren: Provider-Timeouts, Rate Limits, unbekannte Tools, Netzwerkabbrueche. Mit Error Handling faengst Du diese Fehler ab und gibst dem User eine sinnvolle Rueckmeldung, statt die App crashen zu lassen.
Ohne Error Handling: Deine App crasht, wenn der Provider einen Fehler wirft. Der User sieht eine weisse Seite, eine kryptische Fehlermeldung oder die halbe Antwort bricht einfach ab. In Production ist das inakzeptabel — User verlieren Vertrauen und Daten gehen verloren.
Mit Error Handling: Fehler werden abgefangen und in verstaendliche Meldungen uebersetzt. Der User weiss, was passiert ist, und kann es nochmal versuchen. Dein Logging erfasst den Fehler für Debugging. Und mit Retry-Strategien kann Deine App bestimmte Fehler sogar selbst loesen.
WALKTHROUGH
Abschnitt betitelt „WALKTHROUGH“Schicht 1: onError in streamText
Abschnitt betitelt „Schicht 1: onError in streamText“Die einfachste Form: Der onError Callback in streamText wird aufgerufen, wenn waehrend der Generierung ein Fehler auftritt:
import { streamText } from 'ai';import { anthropic } from '@ai-sdk/anthropic';
const result = streamText({ model: anthropic('claude-sonnet-4-5-20250514'), prompt: 'Erklaere Error Handling.', onError({ error }) { // ← Wird bei jedem Fehler aufgerufen console.error('Stream-Fehler:', error); // Hier: Logging, Alerting, Metriken },});onError wird aufgerufen bei:
- Provider-Fehlern: API nicht erreichbar, Rate Limit, Authentication
- Stream-Fehlern: Verbindungsabbruch, Timeout
- Tool-Fehlern: Tool wirft Exception (ab Level 3)
Wichtig: onError verhindert nicht, dass der Fehler zum User durchschlaegt. Es ist ein Hook für Logging und Monitoring — nicht für User-Facing Error Messages.
Schicht 2: onError in toUIMessageStreamResponse
Abschnitt betitelt „Schicht 2: onError in toUIMessageStreamResponse“Web-App Kontext: Der folgende Code zeigt Error Handling in einer Next.js API Route. Deine TRY-Uebung weiter unten arbeitet im Terminal (CLI).
Für Web-APIs kontrollierst Du mit onError in toUIMessageStreamResponse, welche Fehlermeldung der Client sieht:
export async function POST(req: Request) { const { messages } = await req.json();
const result = streamText({ model: anthropic('claude-sonnet-4-5-20250514'), messages, onError({ error }) { // Server-seitiges Logging console.error('[Stream Error]', error); }, });
return result.toUIMessageStreamResponse({ onError(error) { // ← Kontrolliert Client-Fehlermeldung // WICHTIG: Gibt den String zurück, den der Client sieht // Keine internen Details leaken! return 'Es ist ein Fehler aufgetreten. Bitte versuche es erneut.'; }, });}Die Trennung ist entscheidend:
onErrorinstreamText: Server-Seite. Logge den vollen Fehler für Debugging.onErrorintoUIMessageStreamResponse: Client-Seite. Gib eine kurze, verstaendliche Meldung zurück.
Schicht 3: Spezifische Fehlertypen behandeln
Abschnitt betitelt „Schicht 3: Spezifische Fehlertypen behandeln“Das AI SDK exportiert typisierte Error-Klassen. Du kannst Fehler gezielt unterscheiden:
import { streamText, NoSuchToolError } from 'ai';
const result = streamText({ model: anthropic('claude-sonnet-4-5-20250514'), messages, tools: { /* ... */ }, onError({ error }) { if (NoSuchToolError.isInstance(error)) { // ← Typisierte Pruefung console.error(`Unbekanntes Tool: ${error.toolName}`); console.error(`Verfuegbare Tools: ${error.availableToolNames.join(', ')}`); } else { console.error('Unbekannter Fehler:', error); } },});
return result.toUIMessageStreamResponse({ onError(error) { if (NoSuchToolError.isInstance(error)) { return `Das Tool "${error.toolName}" existiert nicht. Verfuegbar: ${error.availableToolNames.join(', ')}`; } return 'Ein unerwarteter Fehler ist aufgetreten.'; },});Wichtige Error-Typen im AI SDK:
| Error-Klasse | Wann | Nuetzliche Properties |
|---|---|---|
NoSuchToolError | LLM ruft ein Tool auf, das nicht existiert | toolName, availableToolNames |
InvalidToolArgumentsError | Tool-Argumente matchen nicht das Schema | toolName, toolArgs |
APICallError | Provider-API antwortet mit Fehler | statusCode, message |
Schicht 4: Retry-Strategie
Abschnitt betitelt „Schicht 4: Retry-Strategie“Für transiente Fehler (Netzwerk, Rate Limits) kannst Du einen Retry-Wrapper bauen:
import { streamText } from 'ai';import { anthropic } from '@ai-sdk/anthropic';
async function streamWithRetry( params: Parameters<typeof streamText>[0], maxRetries = 3,) { let lastError: unknown;
for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const result = streamText(params);
// Stream testen: Das text-Promise awaiten // Wenn der Provider antwortet, ist der Stream OK const fullText = await result.text; // ← Wartet auf kompletten Text return { result, fullText }; // ← Erfolg: Ergebnis zurueckgeben
} catch (error) { lastError = error; console.error(`Versuch ${attempt}/${maxRetries} fehlgeschlagen:`, error);
if (attempt < maxRetries) { const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000); // Exponential Backoff console.log(`Warte ${delay}ms vor Versuch ${attempt + 1}...`); await new Promise((resolve) => setTimeout(resolve, delay)); } } }
throw new Error(`Alle ${maxRetries} Versuche fehlgeschlagen: ${lastError}`);}Die Retry-Strategie nutzt Exponential Backoff: 1s, 2s, 4s, … Das gibt dem Provider Zeit, sich zu erholen (z.B. nach Rate Limiting).
Hinweis: Dieses Pattern wartet auf den vollstaendigen Text (
result.text). Für Streaming-Retries (wo Du den Text schon waehrend der Generierung anzeigen willst) brauchst Du eine komplexere Lösung — z.B. denfor await-Loop in der Retry-Schleife selbst.
Schicht 5: Try/Catch um den Stream-Consumer
Abschnitt betitelt „Schicht 5: Try/Catch um den Stream-Consumer“Vergiss nicht: Fehler können auch beim Konsumieren des Streams auftreten. Wrap den for await Loop in einen try/catch:
const result = streamText({ model: anthropic('claude-sonnet-4-5-20250514'), prompt: 'Erklaere Error Handling.',});
try { for await (const chunk of result.textStream) { process.stdout.write(chunk); } console.log('\n--- Stream erfolgreich beendet ---');} catch (error) { console.error('\n--- Stream abgebrochen ---'); console.error('Fehler:', error); // Fallback: Gespeicherte Antwort zeigen, User informieren, etc. console.log('Die Antwort konnte nicht vollständig geladen werden.');}Aufgabe: Simuliere einen Fehler und fange ihn mit onError ab. Zeige eine User-freundliche Meldung.
Erstelle die Datei error-handling.ts:
import { streamText } from 'ai';import { anthropic } from '@ai-sdk/anthropic';
// TODO 1: Starte streamText mit einem absichtlich falschen Modellnamen// const result = streamText({// model: anthropic('claude-nonexistent-model'),// prompt: 'Dieser Call wird fehlschlagen.',// onError({ error }) {// // TODO 2: Logge den Fehler serverseitig// },// });
// TODO 3: Konsumiere den Stream in einem try/catch// try {// for await (const chunk of result.textStream) {// process.stdout.write(chunk);// }// } catch (error) {// // TODO 4: Zeige eine User-freundliche Fehlermeldung// }Checkliste:
-
streamTextmit einem ungueltigem Modell aufgerufen (oder anderem Fehler-Trigger) -
onErrorCallback implementiert mit Server-seitigem Logging -
for awaitLoop in try/catch gewrapped - User-freundliche Fehlermeldung im catch-Block
Fuehre aus: npx tsx error-handling.ts
Lösung anzeigen
import { streamText } from 'ai';import { anthropic } from '@ai-sdk/anthropic';
console.log('Starte Stream mit absichtlichem Fehler...\n');
const result = streamText({ model: anthropic('claude-nonexistent-model-99'), prompt: 'Dieser Call wird fehlschlagen.', onError({ error }) { console.error('[Server Log] Stream-Fehler aufgetreten:'); console.error('[Server Log]', error); },});
try { for await (const chunk of result.textStream) { process.stdout.write(chunk); }} catch (error) { console.log('\n--- Fehler abgefangen ---'); console.log('Dem User anzeigen: "Die Antwort konnte nicht geladen werden. Bitte versuche es in einigen Sekunden erneut."');
// In einer echten App: // - User-freundliche Meldung in der UI anzeigen // - Retry-Button anbieten // - Fehler an Error-Tracking senden (Sentry, etc.)}Erklärung: Der ungueltige Modellname fuehrt zu einem API-Fehler. onError loggt den Fehler serverseitig (mit allen Details für Debugging). Der try/catch um den Stream-Consumer faengt den Fehler ab und zeigt eine verstaendliche Meldung. In Production wuerdest Du hier eine UI-Komponente rendern statt console.log.
Erwarteter Output (ungefaehr):
Starte Stream mit absichtlichem Fehler...
[Server Log] Stream-Fehler aufgetreten:[Server Log] APICallError: 404 model_not_found ...
--- Fehler abgefangen ---Dem User anzeigen: "Die Antwort konnte nicht geladen werden.Bitte versuche es in einigen Sekunden erneut."Der genaue Fehlertext variiert je nach Provider. Entscheidend ist:
onErrorloggt den vollen Fehler, der try/catch zeigt die User-freundliche Meldung.
COMBINE
Abschnitt betitelt „COMBINE“Uebung: Kombiniere Error Handling mit Stream Transforms. Baue einen Stream, der:
smoothStream()als Transform nutzt- Einen Custom Transform hat, der prueoft ob ein Chunk ein bestimmtes “verbotenes” Muster enthaelt (z.B. “FEHLER_SIMULATION”) und in dem Fall einen Error wirft
onErrorloggt den Fehler serverseitig- Ein try/catch um den Stream-Consumer zeigt eine User-freundliche Meldung
Optional Stretch Goal: Implementiere streamWithRetry mit Exponential Backoff. Provoziere einen Fehler (z.B. mit einem falschen API-Key) und beobachte, wie die Retry-Logik 3 Versuche unternimmt, bevor sie aufgibt. Logge jeden Versuch mit Timestamp.