20 Asynchronität

Im heutigen Web sind asynchrone Operationen allgegenwärtig. Wir laden Daten von einem Server, warten auf Benutzereingaben oder setzen Timer. All diese Operationen können in JavaScript und TypeScript auf asynchrone Weise gehandhabt werden.

Bisher wurden Callback-Funktionen und Promises verwendet, um asynchronen Code zu schreiben. Diese Techniken können jedoch in komplexen Szenarien schwer zu verstehen und zu handhaben sein. Deshalb wurden async und await eingeführt. Sie sind eine Erweiterung von Promises und machen den asynchronen Code lesbarer und einfacher zu schreiben.

20.1 Async-Funktionen

Eine Funktion, die mit dem Schlüsselwort async deklariert wird, gibt immer ein Promise zurück. Wenn das Resultat einer async Funktion ein Wert ist, wird der Wert in ein Promise-Objekt verpackt und zurückgegeben.

async function myFunction() {
  return "Hello World";
}
console.log(myFunction());

20.2 Das Await-Schlüsselwort

Innerhalb einer async Funktion können wir das await Schlüsselwort verwenden, um die Ausführung der Funktion zu pausieren, bis das Promise erfüllt ist. Dies führt zu einem synchron aussehenden Code, der eigentlich asynchron ist.

async function myFunction() {
  let value = await fetchFromServer();
  console.log(value);
}

In diesem Beispiel wird myFunction nicht weiter ausgeführt, bis fetchFromServer() abgeschlossen ist.

Die Funktion, die ein Promise zurückgibt, wird asynchron ausgeführt, was bedeutet, dass sie im Hintergrund abläuft und der Code danach weiter ausgeführt wird. Sie müssen das Promise nicht unbedingt “abholen”. Wenn Sie jedoch das Ergebnis der asynchronen Operation verwenden oder auf ihren Abschluss warten möchten, müssen Sie das Promise abholen.

Zum Beispiel, wenn Sie eine Funktion haben, die ein Promise zurückgibt, und Sie den Rest Ihres Codes ausführen möchten, ohne auf das Ergebnis zu warten, können Sie einfach die Funktion aufrufen und ihr Ergebnis ignorieren:

// ruft die Funktion auf, wartet aber nicht auf ihr Ergebnis
asyncFunction();
// wird sofort ausgeführt, ohne auf asyncFunction zu warten
doOtherStuff();

Aber wenn Sie das Ergebnis verwenden oder auf den Abschluss der asynchronen Funktion warten müssen, dann sollten Sie das Promise mit await abholen oder die then Methode verwenden:

// Mit await
async function main() {
  // wartet auf das Ergebnis
  const result = await asyncFunction();
  // verwendet das Ergebnis
  doOtherStuff(result); 
}
main();

// Mit then
asyncFunction()
  // wird ausgeführt, wenn das Promise erfüllt ist
  .then(result => doOtherStuff(result));

Bitte beachten Sie, dass await nur innerhalb von async Funktionen verwendet werden kann. Es ist auch wichtig zu beachten, dass, obwohl Sie nicht auf ein Promise warten müssen, unerledigte Promises, die abgelehnt werden (z.B. aufgrund eines Fehlers in der asynchronen Operation), einen UnhandledPromiseRejectionWarning in Node.js auslösen können. Daher ist es immer eine gute Praxis, Promises zu behandeln und geeignete Fehlerbehandlungsmechanismen bereitzustellen.

20.3 Fehlerbehandlung

Wir können try/catch-Blöcke verwenden, um Fehler in async/await abzufangen. Wenn ein Fehler in der fetchFromServer Funktion auftritt, wird er durch den catch-Block abgefangen.

async function myFunction() {
  try {
    let value = await fetchFromServer();
    console.log(value);
  } catch (error) {
    console.log(error);
  }
}

20.4 Parallele Ausführung

Mit Promise.all() können wir mehrere Promises gleichzeitig ausführen und auf alle warten, bevor wir fortfahren.

async function myFunction() {
  let [value1, value2] = await Promise.all([fetchFromServer1(), fetchFromServer2()]);
  console.log(value1, value2);
}

In diesem Beispiel warten wir, bis beide fetchFromServer1 und fetchFromServer2 abgeschlossen sind, bevor wir fortfahren.

Async und await sind mächtige Werkzeuge, die uns helfen, asynchronen Code in einer Weise zu schreiben, die leichter zu verstehen und zu verwalten ist. Sie sind eine wichtige Ergänzung zu den vorhandenen Techniken zur Behandlung von Asynchronität in JavaScript und TypeScript. Mit ihnen können wir klarere und weniger fehleranfällige asynchrone Logik schreiben.

20.5 Promise-Handhabung mit then, catch und finally

Promises sind ein zentraler Baustein moderner asynchroner Programmierung in JavaScript und TypeScript. Mit den Methoden then(), catch() und finally() kann man Aktionen planen, die nach dem Erfolg oder Fehlschlagen eines Promises ausgeführt werden sollen.

20.5.1 Die then() Methode

Die then() Methode wird verwendet, um Callbacks anzuhängen, die ausgeführt werden, wenn das Promise erfüllt (erfolgreich abgeschlossen) wird. Die Methode akzeptiert zwei Argumente, beide optional: eine Callback-Funktion für den Erfolgsfall und eine für den Fehlerfall.

let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("Promise erfüllt!"), 1000);
});

promise.then(
    result => console.log(result), // "Promise erfüllt!"
    error => console.log(error)    // Wird nicht ausgeführt
);

20.5.2 Die catch() Methode

Die catch() Methode ist eine verkürzte Form von then(null, errorHandler). Sie wird aufgerufen, wenn das Promise abgelehnt (ein Fehler aufgetreten) wird.

let promise = new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error("Promise abgelehnt!")), 1000);
});

promise.catch(
    error => console.log(error)  // Error: Promise abgelehnt!
);

20.5.3 Die finally() Methode

Die finally() Methode ist ähnlich wie then() und catch(), wird aber unabhängig vom Erfolg oder Misserfolg des Promises ausgeführt. Es ist nützlich für die Bereinigung von Ressourcen oder die Durchführung von Aktionen, die in jedem Fall stattfinden sollten.

let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("Promise erfüllt!"), 1000);
});

promise
    .then(result => console.log(result))  // "Promise erfüllt!"
    .finally(() => console.log("Promise abgeschlossen."));

In diesem Beispiel wird die finally() Methode ausgeführt, nachdem das Promise erfüllt wurde und die then() Methode ihre Arbeit abgeschlossen hat. Wenn das Promise abgelehnt worden wäre, wäre finally() trotzdem aufgerufen worden.

Hinweis: Während then() und catch() ein neues Promise zurückgeben (das potenziell ein anderes Ergebnis oder einen anderen Zustand hat), gibt finally() immer das ursprüngliche Promise zurück.

20.6 async/await vs then/catch/finally

Die Funktionen then, catch und finally sowie async/await sind beide Mechanismen zur Arbeit mit asynchronem Code in JavaScript und TypeScript, speziell zur Arbeit mit Promises. Während then, catch und finally Methoden sind, die direkt an einem Promise aufgerufen werden, ist async/await ein syntaktisches Konstrukt, das die Arbeit mit Promises vereinfacht.

Hier sind einige Schlüsselunterschiede und Zusammenhänge zwischen ihnen:

20.6.1 Syntaktische Unterschiede

then, catch und finally sind Methodenaufrufe, die auf Promises angewendet werden. Sie nehmen Callback-Funktionen als Argumente, die ausgeführt werden, wenn das Promise erfüllt oder abgelehnt wird.

doSomethingAsync()
    .then(result => console.log(result))
    .catch(error => console.log(error))
    .finally(() => console.log('Fertig'));

async/await hingegen ist ein syntaktisches Konstrukt, das es ermöglicht, asynchronen Code zu schreiben, der fast wie synchroner Code aussieht. Ein await-Ausdruck pausiert die Ausführung der Funktion, bis das Promise erfüllt ist, und gibt dann den Wert des Promises zurück.

async function doSomething() {
    try {
        const result = await doSomethingAsync();
        console.log(result);
    } catch (error) {
        console.log(error);
    } finally {
        console.log('Fertig');
    }
}

20.6.2 Anwendungsunterschiede

then, catch und finally können in jedem Kontext verwendet werden, in dem Sie mit Promises arbeiten. Sie sind universell und funktionieren in jeder Funktion und jedem Kontext.

async/await kann jedoch nur in Funktionen verwendet werden, die mit dem async-Schlüsselwort gekennzeichnet sind. Es ist leistungsfähiger und flexibler, aber seine Verwendung ist auf async-Funktionen beschränkt.

20.6.3 Fehlerbehandlung

Mit then und catch können Sie die Erfolgs- und Fehlerfälle jeweils getrennt behandeln. Mit async/await hingegen können Sie die Fehlerbehandlung mit traditionellen try/catch-Konstrukten durchführen, was oft intuitiver und übersichtlicher ist.

Insgesamt sind async/await und then/catch/finally eng miteinander verbunden und dienen dem gleichen Zweck: Die Arbeit mit asynchronem Code und Promises. Welche Sie verwenden, hängt von Ihrem speziellen Anwendungsfall und Ihrer bevorzugten Programmierstil ab.

20.7 Aufgaben

20.7.1 Übungsaufgaben zu async und await:

Aufgabe 1: Schreiben Sie eine async Funktion namens getRandomNumber, die nach 2 Sekunden ein zufälliges Zahl zwischen 1 und 10 zurückgibt. Verwenden Sie dazu das setTimeout Funktion und Promises.

Aufgabe 2: Schreiben Sie eine async Funktion namens fetchUser, die die Daten eines Benutzers von der JSONPlaceholder API (https://jsonplaceholder.typicode.com/users/1) abruft. Verwenden Sie das fetch Funktion und await, um auf die Antwort zu warten. Geben Sie das Ergebnis als JSON aus.

Aufgabe 3: Erweitern Sie die fetchUser Funktion aus Aufgabe 2 mit Fehlerbehandlung. Wenn ein Fehler auftritt, geben Sie “Ein Fehler ist aufgetreten” in der Konsole aus.

Aufgabe 4: Schreiben Sie eine async Funktion namens fetchAllUsers, die die Daten aller Benutzer von der JSONPlaceholder API (https://jsonplaceholder.typicode.com/users) abruft. Verwenden Sie Promise.all, um alle Anfragen parallel zu bearbeiten. Geben Sie das Ergebnis als JSON aus.

Aufgabe 5: Schreiben Sie eine async Funktion namens printUserNames, die die fetchAllUsers Funktion verwendet, um alle Benutzerdaten abzurufen, und dann nur die Namen der Benutzer ausgibt.

20.7.2 Lösungen

Lösung Aufgabe 1:

async function getRandomNumber() {
  return new Promise<number>((resolve) => {
    setTimeout(() => {
      resolve(Math.floor(Math.random() * 10) + 1);
    }, 2000);
  });
}

// logs a random number between 1 and 10 after 2 seconds
getRandomNumber().then(console.log);

Lösung Aufgabe 2:

async function fetchUser() {
  const response = 
    await fetch('https://jsonplaceholder.typicode.com/users/1');
  const user = await response.json();
  console.log(user);
}

fetchUser();

Lösung Aufgabe 3:

async function fetchUser() {
  try {
    const response = 
      await fetch('https://jsonplaceholder.typicode.com/users/1');
    const user = await response.json();
    console.log(user);
  } catch (error) {
    console.error('Ein Fehler ist aufgetreten');
  }
}

fetchUser();

Lösung Aufgabe 4:

async function fetchAllUsers() {
  const response = 
    await fetch('https://jsonplaceholder.typicode.com/users');
  const users = await response.json();
  console.log(users);
}

fetchAllUsers();

Lösung Aufgabe 5:

async function fetchAllUsers() {
  const response = 
    await fetch('https://jsonplaceholder.typicode.com/users');
  return response.json();
}

async function printUserNames() {
  const users = await fetchAllUsers();
  users.forEach(user => console.log(user.name));
}

printUserNames();

20.7.3 Übungsaufgaben zu then, catch und finally:

Aufgabe 1: Erstellen Sie eine Funktion, die ein Promise zurückgibt, das nach einer Verzögerung von 2 Sekunden erfüllt wird. Verwenden Sie die then-Methode, um eine Meldung an die Konsole zu senden, wenn das Promise erfüllt wird.

Aufgabe 2: Erstellen Sie eine Funktion, die ein Promise zurückgibt, das nach einer Verzögerung von 3 Sekunden abgelehnt wird. Verwenden Sie die catch-Methode, um eine Fehlermeldung an die Konsole zu senden, wenn das Promise abgelehnt wird.

Aufgabe 3: Kombinieren Sie die Funktionen aus den Aufgaben 1 und 2. Verwenden Sie die finally-Methode, um eine Meldung an die Konsole zu senden, unabhängig davon, ob das Promise erfüllt oder abgelehnt wird.

Aufgabe 4: Erstellen Sie ein Array von Promises, die alle nach unterschiedlichen Zeitspannen erfüllt werden. Verwenden Sie Promise.all und die then-Methode, um eine Meldung an die Konsole zu senden, wenn alle Promises erfüllt sind.

Aufgabe 5: Ändern Sie die Funktion aus Aufgabe 4 so, dass eines der Promises abgelehnt wird. Verwenden Sie die catch-Methode, um eine Fehlermeldung an die Konsole zu senden, wenn mindestens eines der Promises abgelehnt wird.

20.7.4 Lösungen

Hier sind die Lösungen zu den Aufgaben:

Lösung Aufgabe 1:

function delayedPromise() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Promise wurde erfüllt!');
        }, 2000);
    });
}

delayedPromise().then((message) => {
    console.log(message);
});

Lösung Aufgabe 2:

function rejectedPromise() {
    return new Promise((_, reject) => {
        setTimeout(() => {
            reject('Promise wurde abgelehnt!');
        }, 3000);
    });
}

rejectedPromise().catch((errorMessage) => {
    console.error(errorMessage);
});

Lösung Aufgabe 3:

function combinedPromise() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            Math.random() > 0.5 ? resolve('Promise wurde erfüllt!') : reject('Promise wurde abgelehnt!');
        }, 2000);
    });
}

combinedPromise()
    .then((message) => {
        console.log(message);
    })
    .catch((errorMessage) => {
        console.error(errorMessage);
    })
    .finally(() => {
        console.log('Promise-Operation abgeschlossen!');
    });

Lösung Aufgabe 4:

const promises = [
    new Promise((resolve) => setTimeout(() => resolve('Promise 1 erfüllt!'), 1000)),
    new Promise((resolve) => setTimeout(() => resolve('Promise 2 erfüllt!'), 2000)),
    new Promise((resolve) => setTimeout(() => resolve('Promise 3 erfüllt!'), 3000)),
];

Promise.all(promises).then((messages) => {
    console.log(messages);
});

Lösung Aufgabe 5:

const promisesWithRejection = [
    new Promise((resolve) => setTimeout(() => resolve('Promise 1 erfüllt!'), 1000)),
    new Promise((_, reject) => setTimeout(() => reject('Promise 2 abgelehnt!'), 2000)),
    new Promise((resolve) => setTimeout(() => resolve('Promise 3 erfüllt!'), 3000)),
];

Promise.all(promisesWithRejection)
    .then((messages) => {
        console.log(messages);
    })
    .catch((errorMessage) => {
        console.error(errorMessage);
    });

Bitte beachten Sie, dass dies nur eine mögliche Lösung für jede Aufgabe ist. Es gibt viele Möglichkeiten, die Aufgaben zu lösen, insbesondere wenn man die Variationen in der Fehlerbehandlung und in der Verwaltung asynchroner Abläufe berücksichtigt.