Asynchrone Programmierung ist in modernen Webanwendungen von entscheidender Bedeutung, um lang laufende Prozesse wie Netzwerkanfragen oder Dateizugriffe zu behandeln, ohne die Anwendung zu blockieren. In TypeScript lassen sich asynchrone Aufgaben wie in JavaScript mit Promises und dem async/await-Syntax handhaben. Dabei bringt TypeScript die Vorteile der Typisierung und der statischen Analyse auch in die Welt der asynchronen Programmierung. In diesem Artikel schauen wir uns den Umgang mit Promises, die Typisierung asynchroner Funktionen und die Fehlerbehandlung an.
Umgang mit Promises und async/await
Promises sind in JavaScript und TypeScript der grundlegende Mechanismus, um asynchrone Operationen darzustellen. Ein Promise stellt ein zukünftiges Ergebnis dar, das entweder erfolgreich ist oder fehlschlägt. Die beiden wichtigen Zustände eines Promises sind:
- fulfilled (erfolgreich abgeschlossen)
- rejected (Fehler aufgetreten)
Ein einfaches Beispiel für den Umgang mit Promises:
function fetchData(url: string): Promise<string> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (url === "valid_url") {
resolve("Data received");
} else {
reject("Invalid URL");
}
}, 1000);
});
}
fetchData("valid_url")
.then((data) => {
console.log(data); // Ausgabe: Data received
})
.catch((error) => {
console.error(error);
});
Hier verwendet die Funktion `fetchData` ein Promise, das entweder erfolgreich Daten liefert oder einen Fehler ausgibt. Mit `.then()` wird die Erfolgsverarbeitung beschrieben und `.catch()` behandelt Fehler.
Die moderne und benutzerfreundlichere Art, mit Promises zu arbeiten, ist der async/await-Syntax. Es bietet eine klarere und syntaktisch einfachere Möglichkeit, mit asynchronem Code umzugehen:
async function getData(): Promise<void> {
try {
const data = await fetchData("valid_url");
console.log(data); // Ausgabe: Data received
} catch (error) {
console.error(error);
}
}
getData();
In diesem Beispiel wird `await` verwendet, um die Ausführung der Funktion anzuhalten, bis das Promise aufgelöst ist. async/await macht den Code leichter verständlich und reduziert die sogenannte „Promise-Kette“, die bei verschachtelten `.then()`-Aufrufen entstehen kann.
Typisierung von asynchronen Funktionen
Eine der großen Stärken von TypeScript ist die Möglichkeit, auch asynchrone Funktionen explizit zu typisieren. In TypeScript gibt eine asynchrone Funktion immer ein `Promise` zurück, auch wenn dies nicht explizit im Code steht. Es ist jedoch eine gute Praxis, den Rückgabetyp klar anzugeben, um die Absicht und die erwarteten Ergebnisse besser zu dokumentieren.
Hier ein Beispiel für eine typisierte asynchrone Funktion:
async function getNumber(): Promise<number> {
return 42;
}
async function fetchDataAndProcess(): Promise<void> {
const result: number = await getNumber();
console.log(result); // Ausgabe: 42
}
In diesem Fall gibt die Funktion `getNumber` ein `Promise<number>` zurück, was bedeutet, dass der Rückgabewert vom Typ `number` ist, sobald das Promise erfüllt wird. Die Funktion `fetchDataAndProcess` gibt hingegen ein `Promise<void>` zurück, weil sie selbst keine Daten zurückliefert, sondern nur Aktionen ausführt.
Wenn eine Funktion ein Promise zurückgibt, das mehrere mögliche Typen haben kann, können Union Types verwendet werden, um dies darzustellen:
async function getResult(): Promise<string | number> {
return Math.random() > 0.5 ? "Success" : 42;
}
Diese Funktion kann entweder einen `string` oder eine `number` zurückgeben, je nachdem, welcher Fall eintritt.
Fehlerbehandlung in asynchronem Code
In der asynchronen Programmierung ist die Fehlerbehandlung besonders wichtig, da Fehler nicht sofort auftreten, sondern erst, wenn das Promise aufgelöst oder abgelehnt wird. TypeScript hilft durch seine Typisierung dabei, Fehler klar zu erkennen und besser zu handhaben.
Es gibt zwei Hauptmethoden zur Fehlerbehandlung bei asynchronem Code. Wie bereits im ersten Beispiel gezeigt, können Fehler bei Promises mit der Methode `.catch()` abgefangen werden. Hier wird ein Callback aufgerufen, wenn das Promise abgelehnt wird:
fetchData("invalid_url")
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error("Fehler:", error); // Ausgabe: Fehler: Invalid URL
});
Eine bevorzugte Methode bei async/await ist die Verwendung von `try/catch`, um Fehler direkt im Codeblock zu behandeln. Dies macht die Fehlerbehandlung sauberer und konsistenter, da sie die normale Fehlerbehandlung von synchronem Code nachahmt.
async function getDataWithErrorHandling(): Promise<void> {
try {
const data = await fetchData("invalid_url");
console.log(data);
} catch (error) {
console.error("Fehler:", error); // Ausgabe: Fehler: Invalid URL
}
}
getDataWithErrorHandling();
Der try/catch-Block ermöglicht es, Fehler elegant zu behandeln und den Code zu organisieren, ohne die Lesbarkeit durch Ketten von `.catch()`-Methoden zu beeinträchtigen.
Best Practices bei der Fehlerbehandlung
Wenn du in deinem Code mit mehreren Arten von Fehlern umgehst, solltest du sie spezifizieren, um gezielt auf die Fehler zu reagieren. In TypeScript kannst du spezifische Fehlerklassen definieren, um die Behandlung feiner zu steuern.
class NetworkError extends Error {}
class ValidationError extends Error {}
async function fetchDataWithErrorTypes(url: string): Promise<string> {
if (url !== "valid_url") {
throw new NetworkError("Netzwerkfehler aufgetreten");
}
return "Daten geladen";
}
async function handleErrors() {
try {
const data = await fetchDataWithErrorTypes("invalid_url");
console.log(data);
} catch (error) {
if (error instanceof NetworkError) {
console.error("Spezifischer Fehler:", error.message);
} else {
console.error("Allgemeiner Fehler:", error);
}
}
}
handleErrors();
In diesem Beispiel können unterschiedliche Fehlertypen behandelt werden, was für komplexere Anwendungen hilfreich ist.
Fazit
Asynchrone Programmierung ist entscheidend für die Arbeit mit zeitintensiven Operationen, und TypeScript bringt dank Typisierung zusätzliche Sicherheit in den Umgang mit Promises und async/await. Durch die Typisierung von asynchronen Funktionen bleibt der Code klar und sicher. Die Fehlerbehandlung lässt sich durch try/catch oder `.catch()`-Methoden gezielt und flexibel gestalten. Asynchroner Code in TypeScript wird so nicht nur sicherer, sondern auch besser strukturiert und einfacher zu warten.