Javascript Promise – wenn das Ergebnis etwas Zeit braucht
Ein Promise steht für das Ergebnis einer zukünftigen Operation und taucht überall dort auf, wo eine Aufgabe etwas Zeit braucht – »asynchron« verarbeitet wird. Das Promise ist ein verspricht: Ich liefere später entweder ein Ergebnis oder einen Fehler.
Bild laden mit Promise
Promises werden zur Steuerung asynchroner Vorgänge eingesetzt – Abläufen, deren Ergebnis nicht sofort verfügbar ist, etwa bei Netzwerkzugriffen oder Lesen von Dateien. Sie strukturieren den Skriptcode lesbarer und erlösen das Skript von verschachtelten Callbacks (»Callback Hell«). Zu den Klassikern des Event Handlings gehören das Laden von Daten mit fetch und das nachträgliche Laden eines Bildes als Reaktion auf eine Benutzeranfrage.
await new Promise((resolve, reject) => {}) braucht eine asynchrone Umgebung, also wird der eventListener mit einer asynchronen Funktion geladen.
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
}
document.getElementById("button").addEventListener("click", async () => {
const src = `image.webp?v=${Date.now()}`;
const img = await loadImage(src);
document.querySelector(".demo").append(img);
});
Callbacks bei zeitlich unbestimmbaren Abläufen
Javascript Promise macht den Umgang mit asynchronem Verhalten einfacher und durch die then-Funktion lesbarer als Callback-Funktionen. Asynchron bedeutet, dass zwischen einer Anfrage und der Antwort eine Verzögerung oder Wartezeit von unbestimmter Dauer liegt.
Wenn wir auf einen zeitlich nicht absehbaren Ablauf angewiesen sind – z.B. bei der Antwort von der Anwendung auf dem Server oder beim Laden einer json-Datei – werden im einfachsten Fall Callbacks eingesetzt, die beim Eintreten eines Ereignisses aufgerufen werden.
Wenn nur ein Ladevorgang beobachtet werden muss, ist alles noch einfach. Aber was ist, wenn nach dem Laden von Informationen auf eine weitere Aktion gewartet werden muss, z.B. weil die URL für eine weitere Anfrage in der ersten Antwort steht?
// Callback Hell
getUser(function(user) {
getSettings(user.id, function(settings) {
getPreferences(settings, function(prefs) {
getDashboard(prefs, function(dashboard) {
console.log("Fertig:", dashboard);
}, function(err) {
console.error("Fehler beim Dashboard:", err);
});
}, function(err) {
console.error("Fehler bei Preferences:", err);
});
}, function(err) {
console.error("Fehler bei Settings:", err);
});
}, function(err) {
console.error("Fehler bei User:", err);
});
// .then-Syntax
getUser()
.then(user => getSettings(user.id))
.then(settings => getPreferences(settings))
.then(prefs => getDashboard(prefs))
.then(dashboard => console.log("Fertig:", dashboard))
.catch(err => console.error("Fehler:", err));
Verschachtelte Events
Mit verschachtelten asynchronen Events kommen wir in Teufels Küche, die auch als Callback Hell bezeichnet wird. Auch Javascript fetch würde dieses abgrundtiefe Loch verschachtelter asynchroner Aufrufe nicht wirklich vereinfachen.
Promises sind Objekte, die das Ergebnis einer asynchronen Aktion abfangen und es zurück geben, wenn die versprochenen Daten erfolgreich geladen wurden oder ein Fehler aufgetreten ist. Sie sind sozusagen Platzhalter für eine Zusage, die vielleicht erfüllt wird, vielleicht aber auch nicht.
let p = new Promise ( function (resolve, reject) {
if (alle Aufgaben erfüllt) {
resolve ();
} else {
reject (err);
}
});
Ein Promise ist eine Schutzmaßnahme. Es wird niemals vor dem Ende des Wartens auf die Ausführung einer Aktion aufgerufen und wird nur einmal ausgeführt. Gleich ob ein Promise erfüllt werden kann oder nicht: Es ruft auf jeden Fall die korrekte Methode resolve () bzw. reject () auf. Darüber hinaus können then und catch miteinander verketted (chaining) werden: Dann ruft das Ergebnis einer Operation am Ende die nächste Operation auf.
Promise und .then()
Ein einfaches fetch mit nur einem asynchronen Request ist übersichtlich, aber sobald ein weiterer Request auf dem ersten Request beruht, wird der Code verschachtelt und unübersichtlich.
Promises stellen eine Methode .then() zur Verfügung, die ausgeführt wird, nachdem das Versprechen eingelöst wurde. .then() enthält zwei optionale Argumente: ein Callback für den Erfolg, ein Callback für den Fehlerfall.
┌── callback
▼
p.then( function (result) {
// Promise erfüllt
}, function(err) {
▲
└─ callback
// Promise zurückgewiesen
})
Wenn das Promise erfüllt ist, wird die erste Funktion mit dem übergebenen result-Objekt ausgeführt. Anderenfalls wird die Rückweisung (reject) in der zweiten Funktion mit dem übergebenen error-Objekt ausgeführt.
So kann z.B. eine komplizierte Aktionsfolge aussehen, in der drei JSON-Dateien geladen werden, wobei das Auslesen der zweiten JSON-Datei nur Sinn macht, wenn das erste JSON korrekt angeliefert wurde.
then-Aufrufe können sequentiell aneinander gehangen werden, um zusätzliche asynchrone Aktionen nacheinander auszuführen (chaining). Wird ein Promise zurückgegeben, wartet das nächste then auf das erste Promise, bevor die Ausführung beginnt. Nur durch das return wird das nächste Promise an die Kette übergeben. Ohne return würde die Kette nicht warten.
konzert ("promise/konzerte-10.json")
.then ((events10) => {
plan (events10)
return konzert ("promise/konzerte-11.json");
})
.then ((events11) => {
plan (events11)
return konzert ("promise/konzerte-12.json");
})
.then ((events12) => {
plan (events12)
});
Da haben wir ein einfaches Muster: then gefolgt von einem return, then gefolgt von einem return, alles aufgereiht wie auf einer Perlenschnur und ohne Verschachtelung.
Promise .then() // wenn fulfilled .then() // nächstes Promise .then() .catch() // fängt alles ab
Zuguterletzt noch die Funktion plan (), die alle Konzerte in zeitlicher Folge ausgibt.
function plan(data) {
const ul = document.querySelector("ul.plan1");
const fragment = document.createDocumentFragment();
data.forEach(event => {
const li = document.createElement("li");
li.textContent = `${event.datum} ${event.konzert}`;
fragment.appendChild(li);
});
ul.appendChild(fragment);
}
Die alte Promise-Kette modernisiert
async/await ist eine Schicht »syntaktischer Zucker« über Promises und lässt asynchronen Code wie synchronen aussehen. Das ist funktional identisch, nur lesbarer.
fetch(…).then(…) arbeitet intern immer noch mit Callbacks. Erst mit async / await versteckt das Callback-Muster syntaktisch, aber technisch sind Promises weiterhin callback-basiert.
async function init() {
try {
const events10 = await konzert("promise/konzerte-10.json");
plan(events10);
const events11 = await konzert("promise/konzerte-11.json");
plan(events11);
const events12 = await konzert("promise/konzerte-12.json");
plan(events12);
} catch (error) {
msg(error.message);
}
}
init();
Promise.all () – mehrere Dateien laden
In solchen Situationen wie dem vorangegangenen Beispiel gibt es eine elegante Lösung mit Promise.all (). Das Argument von all() ist ein Array. Die Funktion konzerte () bleibt wie gehabt, aber die Arrays der JSON-Dateien werden einer Variablen zugewiesen.
Promise.all([
fetch('konzerte-1.json').then(res => res.json())
fetch('konzerte-2.json').then(res => res.json())
fetch('konzerte-3.json').then(res => res.json())
])
.then(([event1, event2, event3]) => {
console.log('Alle drei Dateien geladen');
console.log('Daten 1:', event1);
console.log('Daten 2:', event2);
console.log('Daten 3:', event3);
// Jetzt können event1 event2 event3 weiterarbeitet werden
})
.catch(error => {
console.error('Fehler beim Laden der JSON-Dateien:', error);
})
- fetch('...').then(res => res.json()): lädt die Datei und wandelt sie in ein JS-Objekt um.
- Promise.all([...]): wartet, bis alle Promises erfüllt sind.
- .then(([event1, event2, event3]) => {...}): erhält die Inhalte aller drei Dateien als Array (in derselben Reihenfolge wie in Promise.all).
- .catch(...): fängt alle Fehler ab (z. B. wenn eine Datei nicht gefunden wird).