DIgilife - stock.adobe.com

Asynchrones JavaScript: Callbacks, Promises und Async/Await

Asynchrones JavaScript ist sinnvoll für moderne Webentwicklung. Callbacks, Promises und Async/Await bieten effiziente Abläufe. Der Beitrag zeigt, wie die Befehle funktionieren.

JavaScript ist eine Sprache, die asynchron arbeiten kann, da sie ein Single-Threaded-Modell mit einer ereignisgesteuerten Architektur kombiniert. Das bedeutet, dass JavaScript nicht mehrere Aufgaben gleichzeitig auf verschiedenen Threads ausführt, sondern eine Ereignisschleife (Event Loop) nutzt, um nicht-blockierende Operationen zu verwalten.

Ein zentrales Konzept dabei ist das Nicht-Blockieren der Hauptausführung. Während synchroner Code Zeile für Zeile abgearbeitet wird und den Ablauf stoppt, bis eine Operation abgeschlossen ist, ermöglicht Asynchronität, dass zeitaufwendige Aufgaben wie Netzwerkaufrufe, Dateizugriffe oder Timer im Hintergrund ausgeführt werden, während das Programm weiterläuft.

Dieses Verhalten wird durch Callbacks, Promises und Async/Await gesteuert, die JavaScript erlauben, mit Verzögerungen oder langen Rechenoperationen umzugehen, ohne den gesamten Ablauf zu unterbrechen. Die Kombination aus Event Loop, Call Stack und Web API sorgt dafür, dass Aufgaben delegiert, wieder aufgenommen und in der richtigen Reihenfolge ausgeführt werden.

JavaScript und Webanwendungen

Diese Architektur macht JavaScript ideal für interaktive Webanwendungen, die Benutzereingaben schnell verarbeiten und gleichzeitig auf externe Ressourcen wie Datenbanken oder APIs zugreifen müssen. Das bedeutet, dass Code nicht Zeile für Zeile in einer festen Reihenfolge ausgeführt wird. Stattdessen kann JavaScript bestimmte Aufgaben verzögert oder parallel bearbeiten, ohne den gesamten Codefluss zu blockieren. Das Konzept ist besonders wichtig für Webanwendungen, bei denen Nutzerinteraktionen und Serverkommunikationen oft asynchron erfolgen. Es gibt drei Hauptmechanismen, um asynchronen Code in JavaScript zu verwalten:

  • Callbacks
  • Promises
  • Async/Await

Jeder dieser Ansätze hat seine eigenen Stärken und Schwächen, die wir nachfolgend betrachten.

Callbacks: Grundform der Asynchronität

Ein Callback ist eine Funktion, die als Argument an eine andere Funktion übergeben wird. Diese wird nach Abschluss einer asynchronen Operation aufgerufen. Ein einfaches Beispiel für einen Callback ist die setTimeout-Funktion:

setTimeout(function() {
    console.log("Wartezeit abgelaufen!");
}, 1000);

Hier wird eine anonyme Funktion nach einer Sekunde ausgeführt. Callbacks sind auch in Event-Handlern häufig zu finden, etwa bei addEventListener:

button.addEventListener("click", function() {
    console.log("Button wurde geklickt!");
});

Callbacks können allerdings zu unübersichtlichen und schwer zu wartenden Code führen, insbesondere wenn mehrere asynchrone Operationen voneinander abhängen. Dies führt zur sogenannten Callback-Hell:

setTimeout(function() {
    console.log("Schritt 1 abgeschlossen");
    setTimeout(function() {
        console.log("Schritt 2 abgeschlossen");
        setTimeout(function() {
            console.log("Schritt 3 abgeschlossen");
        }, 1000);
    }, 1000);
}, 1000);

Hier führen die verschachtelten Callbacks zu schwer lesbarem Code. Um diese Problematik zu lösen, wurden Promises eingeführt.

Promises: strukturierte Asynchronität

Ein Promise ist ein Objekt, das einen zukünftigen Wert repräsentiert. Es kann sich in drei Zuständen befinden:

  • pending (wartend)
  • resolved (erfüllt)
  • rejected (abgelehnt)

Ein einfaches Promise sieht so aus:

let meinPromise = new Promise(function(resolve, reject) {
    let zufallszahl = Math.random();
    if (zufallszahl > 0.5) {
        resolve("Erfolg: Die Zahl war größer als 0.5");
    } else {
        reject("Fehler: Die Zahl war kleiner oder gleich 0.5");
    }
});

Um mit Promises zu arbeiten, werden then() und catch() genutzt:

meinPromise.then(function(message) {
    console.log(message);
}).catch(function(error) {
    console.log(error);
});

Node.js bietet das fs-Modul, um Dateien zu lesen. Statt mit Callbacks kann dies mit Promises erfolgen:

const fs = require("fs").promises;
fs.readFile("datei.txt", "utf-8")
    .then(data => console.log(data))
    .catch(error => console.error("Fehler: ", error));

Hier können Fehler direkt mit catch() abgefangen werden, was den Code lesbarer macht. Ein weiterer Vorteil von Promises ist, dass sie verkettet werden können:

fetch("https://pokeapi.co/api/v2/pokemon/ditto")
    .then(response => response.json())
    .then(data => console.log("Pokémon-Name: ", data.name))
    .catch(error => console.error("Fehler: ", error));

Hier wird der JSON-Inhalt extrahiert, bevor er in der Konsole ausgegeben wird. Diese Verkettung macht den Code sauberer als verschachtelte Callbacks.

Async/Await: die modernste Methode

async und await sind eine Weiterentwicklung von Promises und ermöglichen eine noch einfachere Syntax. Eine Funktion wird als async markiert und innerhalb dieser kann await verwendet werden, um auf ein Promise zu warten:

async function ladeDaten() {
    try {
        let response = await fetch("https://pokeapi.co/api/v2/pokemon/ditto");
        let data = await response.json();
        console.log("Pokémon-Name: ", data.name);
    } catch (error) {
        console.error("Fehler: ", error);
    }
}

Hier pausiert JavaScript die Funktion, bis die fetch-Operation abgeschlossen ist, ohne den Rest des Codes zu blockieren. Fehler werden in einem try/catch-Block behandelt. Auch das Lesen von Dateien lässt sich mit async/await verbessern:

const fs = require("fs").promises;
async function leseDatei() {
    try {
        let data = await fs.readFile("datei.txt", "utf-8");
        console.log(data);
    } catch (error) {
        console.error("Fehler beim Lesen der Datei:", error);
    }
}

Die await-Syntax reduziert die Notwendigkeit von then() und macht den Code lesbarer.

Promise-Chaining versus Async/Await

Vergleicht man beide Methoden, ergeben sich schnell die Unterschiede.

Der Promise-Code:

fetch("https://pokeapi.co/api/v2/pokemon/ditto")
    .then(response => response.json())
    .then(data => console.log("Pokémon-Name: ", data.name))
    .catch(error => console.error("Fehler: ", error));

Der Async/Await-Code:

async function holePokemon() {
    try {
        let response = await fetch("https://pokeapi.co/api/v2/pokemon/ditto");
        let data = await response.json();
        console.log("Pokémon-Name: ", data.name);
    } catch (error) {
        console.error("Fehler: ", error);
    }
}

Der zweite Ansatz ist leichter zu lesen und einfacher zu debuggen. Mit diesen Konzepten kann JavaScript effizient asynchron arbeiten und moderne Web- und Serveranwendungen reaktionsfähiger machen.

Erfahren Sie mehr über Softwareentwicklung