Javascript Promise – wenns funktioniert hat, dann … sonst

Promises werden zur Steuerung asynchroner Vorgänge eingesetzt – also Abläufen, deren Ergebnis nicht sofort verfügbar ist, etwa bei Anfragen an den Server oder Lesen von Dateien. Promises strukturieren den Skriptcode mit async/await lesbarer und erlösen das Skript von verschachtelten Callbacks (»Callback Hell«).

promise then catch

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 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));

Javascript Promise

Mit verschachtelten asynchronen Events kommen wir in Teufels Küche, die auch als Callback Hell bezeichnet wird. Auch jQuery und Javascript fetch würden 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 einfacher XMLHttpRequest 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.

p.then( function (result) {
	// Promise erfüllt
}, function(err) {
	// 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.

    Alternativ kann das Callback aufgesplittet werden und catch übernimmt das Zurückweisen des Promise. Die Methoden können verkettet und sequentiell ausgeführt werden. Wenn etwas von then zurückgegeben wird, wird es dem nächsten then übergeben.

    function konzert (json) {
    	return new Promise ((resolve, reject) => {
    		const xhr = new XMLHttpRequest ();
    		xhr.open ("GET", json);
    		xhr.onreadystatechange = function () {
    			if (this.readyState === 4)  {
    				if (this.status === 200) {
    					resolve (JSON.parse (this.responseText))	
    				} else {
    					reject ("konnte nicht geladen werden");
    				}
    			}
    		}
    		xhr.send();
    	});
    }
    

    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.

    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.

    In diesem Beispiel ist übrigens kein catch-Block mit im Spiel – auch das geht ohne Weiteres, denn das catch ist optional. Es sollte nur dafür gesorgt werden, dass die Anwendung »sanft« verweigert.

    Zuguterletzt noch die Funktion plan (), die alle Konzerte in zeitlicher Folge ausgibt.

    function plan (data) {
    	data.forEach (event => {
    		const li = document.createElement ("li");
    		li.innerText = `${event.datum} ${event.konzert}`;
    		document.querySelector ("ul.plan1").append (li)
    	})
    }
    

    Promise vs Event Listener

    Promises ähneln Event Listenern mit ihrem asynchronen Callback in gewisser Hinsicht. Wir legen fest, was bei der Erfüllung oder bei einem Fehler passieren soll und der Callback wartet auf den Ausgang des Promise.

    Der Unterschied: Während Events mehrmals feuern können, findet ein Promise nur einmal statt.

    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).

    fetch gibt ein Promise zurück

    Der XMLHttpRequest ist aus der Zeit gefallen. Mit fetch muss nicht new Promise notiert werden, denn fetch gibt ein Promise zurück und vereinfacht das Laden von Daten und die API-Programmierung. Dank fetch und async await ist es selten geworden, dass wir eigene Promise-Objekte einsetzen.

    Suchen auf mediaevent.de