Eine vergleichsweise neue Entwicklung im Reich von ECMAScript 6 ist die Einführung von nativen Promises. Nachdem es schon lange Promises in JavaScript-Libraries gab, werden die praktschen Async-Helferlein in ES6 zum Sprachfeature erhoben. Die gute Nachricht dabei: die neuen Standard-Promises verhalten sich genau wie die alten JS-Promises (und auch wirklich nur wie die guten) und sie sind auch schon in einigen aktuellen Browsern implementiert.

Warum Promises? Warum native Promises?

Promises sind Objekte, die asynchrone Operationen kapseln. Sie zwängen asynchrone Operationen in eine einheitliche API, können verarbeitet werden bevor die Operation abgeschlossen ist und es gibt zahlreiche Möglichkeiten Promises zu kombinieren oder in Sequenzen zu verwenden. Sie haben sich (in Browser-JavaScript) als die Alternative zu Callback-Gewurschtel herausgebildet und werden unter anderem von jQuery unterstützt:

// Herkömmlicher Callback
$.get('/answer', function(data){
  console.log(data);
});


// Promise-Objekt
var ajaxPromise = $.get('/answer');

// Promise-Objekt mit Callbacks und Verkettung
ajaxPromise
  .then(function promiseCallback(result){
    return JSON.parse(result);
  })
  .then(function erfolgCallback(parsed){
    window.alert(parsed.answer); // > 42
  }, function fehlerCallback(err){
    console.error(err);
  });

Die Details zu Promises kann man an anderer Stelle in epischer Breite nachlesen. Die eigentlich interessante Frage ist, warum man überhaupt mit ECMAScript 6 native Promises einführen sollte, wenn es doch jQuery (und viele andere Libraries für Promises) gibt. Mindestens vier gute Gründe lassen sich nennen:

  1. Kompatibilität: Die meisten Promise-Libraries produzieren Promises, die weitgehend kompatibel zueinander sind, aber es gibt Ausnahmen. Besonders jQuery tut sich hier negativ hervor und fabriziert Promises, die sehr andere Eigenschaften haben als in den meisten anderen Libraries. Außerdem hat jede Library eine ganz eigene API für die Erstellung von Promises. Vereinheitlichung an beiden Fronten wäre wünschenswert.
  2. Performance: Wenn der Browser nativ Promises spricht, braucht man keine Extra-Library hierfür mehr in die Seite zu laden.
  3. Browser-APIs: Native Promises können vom Browser direkt durch die divseren HTML5-APIs erzeugt und konsumiert werden. Funktionen wie geolocation.getCurrentPosition() könnten Geolocation-Promises erzeugen und die ulta-asynchronen Funktionen der Indexed DB könnten mit Promises statt des üblichen Callback-Krawalls viel übersichtlicher werden.
  4. Gute Webstandards-Praxis: Es ist im Reich der Webstandards üblich, etablierte Patterns in den Standard aufzunehmen und so Hacks durch native Features zu ersetzen. So gibt es heute <video> statt Flash-Filmchen, document.querySelectorAll() statt $() und eben native Promises anstelle von JavaScript-Promises.

Da Promises vom Prinzip her sehr einfache Konstruktionen sind, gibt es bereits nennenswerte Browserunterstützung für dieses ES6-Feature und für uns einen guten Grund, native Promises unter die Lupe zu nehmen.

Die Promise-API in ES6

Die API für die Erzeugung von ES6-Promises gleicht der API von rsvp.js:

var promise = new Promise(function(resolve, reject){
  resolve(42);
});

promise.then(function erfolgCallback(x){
  window.alert(x);
});

Die Promises verhalten sich wie jene aus den zu Promises/A+ kompatiblen JS-Libraries; sie sind garantiert asynchron, fangen in Then-Callbacks geworfene Fehler ein und haben generell keine der aus jQuery bekannten Merkwürdigkeiten:

function fail(){
  throw new Error('Epic fail!');
}

var promise = new Promise(function(resolve, reject){
  resolve(42); // Garantiert asynchron
});

promise
  .then(fail) // Error wird sauber eingefangen und nicht geworfen
  .then(function(){
    window.alert('Epic win!');
  }, function(err){
    window.alert(err.toString());
  });

window.alert('Test'); // Passiert garantiert VOR Promise-Auflösung

Neben dem altbekannten then(erfolgCb, fehlerCb) haben Promise-Objekte noch eine catch()-Methode, die das Definieren eines Fehler-Callbacks ohne Angabe eines Erfolgs-Callbacks erlaubt; catch(cb) ist syntaktischer Zucker für then(undefined, cb).

Der Promise-Constructor bietet außerdem noch die folgenden statischen Methoden:

  1. Promise.all(list) nimmt als Parameter eine Liste von Promises an. Die Funktion gibt ein Promise zurück, das mit einem Array der Werte der augelösten Promises aufgelöst wird. Wird eins der Elemente in der Liste abgelehnt, wird das Promise mit dem entsprechenden Wert ebenfalls abgelehnt.
  2. Promise.race(list) nimmt als Parameter eine Liste von Promises an. Die Funktion gibt ein Promise zurück, das mit den Wert des ersten aufgelösten bzw. abgelehnten Promise aufgelöst bzw. abgelehnt wird.
  3. Promise.resolve(x) gibt ein Promise zurück, das umgehend mit x aufgelöst wird.
  4. Promise.reject(x) gibt ein Promise zurück, das umgehend mit x abgelehnt wird.

Da ECMAScript 6 noch längst kein fertiger Standard ist, sind Änderungen an diesen APIs natürlich nicht auszuschließen. So lange sich die ES6-Promises wie A+-Promises verhalten wäre das aber kein großes Problem, denn dann helfen wieder die guten alten Promise-Libraries.

Eine Zukunft für Promise-Libraries

Q, RSVP und all die anderen Promise-Libraries sind mit der Einführung von nativen Promises keinesfalls reif für die Rente. Alles, was ES6 in Sachen Promises liefert, ist eine Implementierung des Features „Promise“ und ein recht kleines Set an Promise-Tools (Promise.all(), Promise.race()). Alle nennenswerten Promise-JavaScript-Libraries sind hingegen schwerpunktmäßig Tool-Sammlungen und haben nur nebenher auch eine Promise-Implementierung an Bord. Zu den Features von Q zählt z.B.:

  • Promise-Inspektions-Methoden wie promise.isFulfilled() und promise.isRejected()
  • promise.spread(), das wie then() funktioniert, aber den Inhalt eines als Array übergebenen Parameters als Einzel-Parameter an den Callback übergibt
  • Die Möglichkeit, NodeJS-Callbacks und jQuery-„Promises“ bequem in richtige Promises zu verwandeln

Promise-Poweruser werden auf derartige Tools auch in Zukunft nicht verzichten wollen, womit die Promise-Libraries weiterhin eine Existenzberechtigung haben. Sie werden vielleicht schlanker, aber sie werden uns erhalten bleiben.

Browserunterstützung und Fazit

Obwohl Promises als ES6-Feature noch gar nicht so alt sind, gibt es bereits Unterstützung in einigen Browsern. Opera und Chrome (auch mobile) haben das Feature schon länger im Angebot, Firefox (auch mobile) kann seit Version 29 damit aufwarten. Internet Explorer, Safari und der Android-Browser unterstützen Promises erwartungsgemäß nicht. Dieser löchrige Support ist jedoch kein Problem. Selbst wenn jeder Browser schon Promises unterstützen würde, würde man, wie schon erwähnt, aufgrund der vielen Komfort-Funktionen kaum auf die diversen Promise-Libraries verzichten wollen. Da diese Libraries ihre eigenen Promise-Implementierungen mitbringen, ist jetzt wie auch in Zukunft für flächendeckende Unterstützung gesorgt.

Das eigentliche Feature der ES6-Promises ist die Möglichkeit, dass sie jetzt auch native Browser-APIs verwendet bzw. angeboten werden können. Insofern ist die Einführung von nativen Promises vor allem eine Infrastruktur-Maßnahme und vielleicht gar nicht so sehr ein Features für die JavaScript-Entwickler selbst.