Was Sie Ihrer KI zeigen müssen, damit sie produktionsreifen Code erzeugt
KI-Assistenten schreiben Code, der auf den ersten Blick funktioniert. Sie generieren Funktionen, die Daten abrufen, verarbeiten und speichern. Doch sobald etwas schiefgeht – und in Produktionssystemen geht immer etwas schief – zeigt sich die Schwäche: Der Code hat keine robuste Fehlerbehandlung.
Das Problem liegt nicht an der KI selbst. Es liegt daran, dass wir ihr nicht zeigen, wie komplex asynchrone Fehlerbehandlung in TypeScript wirklich ist. Und genau darum geht es in diesem Artikel: Was müssen Sie Ihrem KI-Assistenten beibringen, damit er Code erzeugt, der nicht nur im Happy Path funktioniert, sondern auch in Produktion standhält?
Lieber Video gucken anstatt Artikel lesen?
Das Problem: Fehlerbehandlung in asynchronem Code ist nicht trivial
Fangen wir mit dem klassischen Ansatz an. Sie möchten Benutzerdaten von einer API abrufen und in einer Datenbank speichern. Hier ist, was die meisten KIs ohne spezifische Anweisungen produzieren:
async function saveUserData(userId: string): Promise<void> {
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
await database.save(userData);
console.log('Daten gespeichert');
} catch (error) {
console.error('Ein Fehler ist aufgetreten:', error);
}
}
Dieser Code funktioniert, aber er hat ernsthafte Probleme. Was ist, wenn die API einen 404 zurückgibt? Was, wenn die JSON-Parsing fehlschlägt? Was, wenn die Datenbank nicht erreichbar ist? All diese Fehler landen im selben catch-Block, und wir haben keine Möglichkeit, sie unterschiedlich zu behandeln.
Verschachtelte try-catch-Blöcke
Wenn wir versuchen, verschiedene Fehlertypen unterschiedlich zu behandeln, wird es schnell unübersichtlich:
async function saveUserData(userId: string): Promise<void> {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
console.error('API-Fehler:', response.status);
return;
}
let userData;
try {
userData = await response.json();
} catch (parseError) {
console.error('JSON-Parsing fehlgeschlagen');
return;
}
try {
await database.save(userData);
} catch (dbError) {
console.error('Datenbank-Fehler');
return;
}
console.log('Daten gespeichert');
} catch (error) {
console.error('Netzwerk-Fehler:', error);
}
}
Jetzt behandeln wir Fehler spezifischer, aber der Code ist kaum noch lesbar. Wir haben mehrere Ebenen von try-catch, frühe return-Statements und verlieren die Übersicht über den Kontrollfluss. Und das Schlimmste: Die Fehlertypen sind alle unknown
, TypeScript kann uns nicht helfen.
Promise-Chaining hilft nur begrenzt
Vielleicht versuchen Sie es mit Promise-Chaining:
function saveUserData(userId: string): Promise<void> {
return fetch(`/api/users/${userId}`)
.then(response => {
if (!response.ok) throw new Error('API-Fehler');
return response.json();
})
.then(userData => database.save(userData))
.then(() => console.log('Daten gespeichert'))
.catch(error => console.error('Fehler:', error));
}
Das sieht sauberer aus, aber wir haben das Problem nur verschoben. Alle Fehler landen wieder im selben catch-Block, und wenn wir Zwischenergebnisse für spätere Schritte benötigen, müssen wir sie in äußeren Variablen speichern.
Die Realität: Produktionscode ist komplexer
In echten Anwendungen haben Sie nicht nur einen API-Call und eine Datenbank-Operation. Sie haben mehrere aufeinanderfolgende Schritte, jeder mit eigenen Fehlermöglichkeiten:
async function processUserOrder(orderId: string): Promise<void> {
try {
// Schritt 1: Bestellung abrufen
const order = await fetchOrder(orderId);
// Schritt 2: Benutzer validieren
const user = await validateUser(order.userId);
// Schritt 3: Zahlung durchführen
const payment = await processPayment(order.amount, user.paymentMethod);
// Schritt 4: Bestand aktualisieren
await updateInventory(order.items);
// Schritt 5: Bestätigungs-E-Mail senden
await sendConfirmationEmail(user.email, order);
// Schritt 6: Audit-Log schreiben
await logTransaction(orderId, payment.id);
} catch (error) {
// Was tun wir hier? Rollback? Welche Schritte waren erfolgreich?
console.error('Irgendwas ist schiefgelaufen:', error);
}
}
An welchem Punkt ist der Fehler aufgetreten? Müssen wir eine Zahlung rückgängig machen? Wurde der Bestand bereits aktualisiert? Diese Fragen kann der Code nicht beantworten, weil alle Fehlerinformationen verloren gehen.
Die Lösung: AsyncResult macht Fehler explizit
Hier kommt AsyncResult
aus dem npm-Modul ts-results-es
ins Spiel. AsyncResult ist ähnlich zu Promise<Result<T, Error>>
, hat aber einen entscheidenden Vorteil: Sie müssen das Promise nicht mit await
auflösen, um mit .andThen()
weiterzumachen.
Das klingt erst mal nach einem kleinen Detail, hat aber weitreichende Konsequenzen. Sie können asynchrone Operationen verketten, ohne auf jede einzelne zu warten, und dabei bleibt die Fehlerbehandlung typsicher und explizit.
Wie sieht das konkret aus?
Schauen wir uns ein einfaches Beispiel an:
import { AsyncResult, Ok, Err } from 'ts-results-es';
type UserData = {};
type ApiError = { type: 'api'; status: number };
type ParseError = { type: 'parse'; message: string };
function fetchUserData(
userId: string,
): AsyncResult<UserData, ApiError | ParseError> {
return Ok(userId)
.toAsyncResult()
.andThen((userId) =>
fetch(`/api/users/${userId}`).then((response) =>
response.ok
? Ok(response)
: Err({
type: 'api',
status: response.status,
} satisfies ApiError),
),
)
.andThen((response) =>
response
.json()
.then((data) => Ok(data as UserData))
.catch(() =>
Err({
type: 'parse',
message: 'Invalid JSON',
} satisfies ParseError),
),
);
}
Was haben wir gewonnen? Erstens: Die Fehlertypen sind explizit. TypeScript weiß genau, welche Fehler auftreten können. Zweitens: Wir verketten Operationen mit .andThen()
, ohne jedes Mal await
zu schreiben. Drittens: Fehler propagieren automatisch – wenn ein Schritt fehlschlägt, werden die folgenden übersprungen.
Verkettung ohne Verschachtelung
Jetzt können wir mehrere Operationen elegant verketten:
type DbError = { type: 'db'; message: string };
function saveUserData(
userId: string,
): AsyncResult<void, ApiError | ParseError | DbError> {
return fetchUserData(userId)
.andThen(validateUserData)
.andThen(saveToDatabase)
.andThen(() => Ok.EMPTY);
}
Jeder Schritt kann seinen eigenen Fehlertyp haben, und TypeScript stellt sicher, dass wir alle möglichen Fehler behandeln. Wenn fetchUserData
fehlschlägt, werden validateUserData
und saveToDatabase
gar nicht erst ausgeführt.
Fehler anreichern mit Kontext
Mit .mapErr()
können Sie Fehler mit zusätzlichem Kontext versehen:
function processUserOrder(
orderId: string,
): AsyncResult<Receipt, OrderError> {
return fetchOrder(orderId)
.mapErr((err) => ({ ...err, context: 'fetchOrder', orderId }))
.andThen((order) =>
validateUser(order.userId).mapErr((err) => ({
...err,
context: 'validateUser',
orderId,
})),
)
.andThen((user) =>
processPayment(order.amount, user.paymentMethod).mapErr(
(err) => ({
...err,
context: 'processPayment',
orderId,
}),
),
)
.andThen((payment) => createReceipt(order, payment));
}
Jetzt sehen Sie sofort, in welchem Schritt der Fehler aufgetreten ist und haben alle relevanten IDs zur Hand.
Was Sie Ihrer KI beibringen müssen
Damit Ihr KI-Assistent Code wie oben generiert, müssen Sie ihm sehr spezifische Anweisungen geben. KIs haben eine starke Tendenz, den einfachsten Weg zu gehen – und das ist try-catch mit console.log.
Wenn Sie schreiben: "Schreibe eine Funktion, die Benutzerdaten von einer API abruft und in der Datenbank speichert", bekommen Sie genau den Code, den wir am Anfang gesehen haben. Die KI weiß nicht, dass Sie Wert auf robuste Fehlerbehandlung legen.
Ich implementiere gerade einen Model Context Protocol Server, der all diese Regeln schon enthält. Diesen bekommen Sie am Ende dieser Serie von Blog Posts, so dass Sie Ihren KI-Assistenten nicht mehr extra zu trainieren oder mit extra-smarten Prompts zu "kommandieren" brauchen. Der KI-Assistent "sieht" dann den MCP-Server und benutzt ihn als Tool.
Der Aufwand lohnt sich. Mit einem klaren Error-Handling-Pattern generiert Ihre KI Code, der nicht nur im Happy Path funktioniert, sondern auch die unvermeidlichen Fehler in Produktionssystemen robust behandelt.
Zusammenfassung: KI + AsyncResult = produktionsreifer Code
Asynchrone Fehlerbehandlung in TypeScript ist komplex. Try-catch-Blöcke führen zu unlesbarem Code, Fehlertypen gehen verloren, und es ist schwer, zwischen verschiedenen Fehlerszenarien zu unterscheiden.
AsyncResult aus ts-results-es
löst diese Probleme, indem es Fehler zu einem expliziten Teil des Typsystems macht. Sie können asynchrone Operationen mit .andThen()
verketten, ohne auf jede einzelne zu warten, und dabei bleibt jeder Fehler typsicher und nachvollziehbar.
Weiterführende Ressourcen
Die Dokumentation von ts-results-es
finden Sie auf npm. Wenn Sie tiefer in funktionale Programmierung in TypeScript einsteigen möchten, lohnt sich auch ein Blick auf Libraries wie Effect-TS, die ähnliche Konzepte noch weiter treiben.
Der Schlüssel zu produktionsreifem KI-generierten Code liegt nicht nur in besseren Modellen, sondern in besseren Tools und Prompts. Und jetzt wissen Sie, was Sie an Wissen über robuste Fehlerbehandlung in Ihre Tools oder Prompts verpacken müssen.
Wie es weitergeht
In der nächsten Folge zeige ich Ihnen, was Sie Ihrem KI-Assistenten über die einzelnen Bounded Contexts in Ihrem DDD-Modell mitteilen müssen. Schließlich soll er sie ja reproduzierbar abbilden, als Module mit Service oben, Aggregate in der Mitte, und Repository unten.
Bitte kommentieren Sie weiter unten: Was möchten Sie in dieser Serie noch gern behandelt haben? Welche Fragen haben Sie? Was haben Sie selbst schon probiert? Was davon hat gut geklappt, und was waren Ihre schlimmen Erlebnisse mit KI-Assistenten?
Ich freue mich auf das, was Sie zu sagen haben. Wenn Sie neue Folgen der Serie automatisch per Email haben wollen, abonnieren Sie sie doch gleich hier!
Kommentare