Asynchrones Programmieren erhält in Zeiten der Web-Apps immer grössere Bedeutung. Nehmen wir folgende, unschuldig aussehende Funktion, die irgendwelche Daten holt und zurückliefert (Pseudocode):

  function fetchData(){
      a=fetchSomething()
      /* irgendwelche Zwischenschritte */
      return a
    }

    Hauptprogamm(){
      coolData=fetchData()
      display(coolData)
      /* Mach weiter, update UI */
    }

So weit, so einfach und klar. Das funktioniert super, wenn fetchSomething() ausreichend schnell ist. Doch was, wenn diese Zugriffe über eine langsame Internet-Verbindung gehen? Dann scheint das Hauptprogramm bei fetchData() für den Anwender eingefroren zu sein, während es auf die Anlieferung der Daten wartet.

Um das zu verhindern, entwickelte man die Technik der asynchronen Programmierung. Da JavaScript als klassische Webapp-Sprache, die typischerweise single threaded im Browser abläuft, besonders stark mit diesem Problem konfrontiert ist, ist asynchrone Programmierung in JavaScipt auch am weitesten Verbreitet.

Wir schreiben den Code also so um:

    function fetchData(callback){
      a=fetchSomething()
      /* irgendwelche Zwischenschritte */
      callback(undefined, a)
      /* bzw, wenn ein Fehler passiert wäre; */
      callback(error,undefined)
    }

    Hauptprogramm(){
      fetchData(doDisplay)
      /* erledige andere Dinge, halte das UI am Leben */
    }

    function doDisplay(error,coolData){
      if(error){
        // Ooops!
      }else{
        display(coolData)
      }
    }

Viel besser. Die Funktion fetchData() kehrt sofort zurück und das Hauptprogramm kann normal weiter laufen. Wenn das Resultat da ist, wird die beim Aufruf angegebene callback-Funktion “doDisplay” mit dem Resultat aufgerufen und die Anwendung kann sich dann um die Wieterverarbeitung kümmern. Per Konvention ist bei diesen “NodeJS-Style” Callbacks der erste Parameter immer ein Fehler-Objekt oder eine Fehlermeldung.

Man kann das mit anonymen Funktionen noch ein wenig übersichtlicher gestalten:

    Hauptprogramm(){
      fetchData(function(error,coolData){
        if(!error){
          display(coolData)
        }
      })
    }

Das ist funktional exakt dasselbe, aber einfacher lesbar. Und mit dieser Programmiertechnik haben Web-Entwickler seit vielen Jahren problemlos das Web 2.0 aufgebaut. Allerdings wurden mit zunehmender Komplexität der Web-Apps auch die Klagen über die “Callback-Hölle” immer lauter. Was ist damit gemeint?

Nehmen wir mal an, wir benötigen nicht nur ein, sondern mehrere externe Datenquellen zum Aufbau unserer Anzeige. Nennen wir dieObjekte, die wir benötigen: A, B und C. Das würden wir so programmieren:

       function fetchABC(callback){
        fetchA(function(A){
          /* Irgendwelche Zwischenschritte */
          fetchB(function(B){
            /* irgendwelche Zwischenschritte */
            /* oops da ist ein Fehler passiert */
            fetchC(function(C){
              /* Aufbereitung der Daten */
              callback(coolResult)
              })
            })
          })
       }

      Hauptprogramm(){
        fetchABC(function(error,coolData){
            display(coolData)
          })
        }

Es gibt hier zwei Probleme:

  • Der Code wird zu Spaghetticode und zunehemd schlechter lesbar

  • Fehlerbehandlung wird schwierig. Was soll man mit dem Fehler anstellen, der in irgendeiner der Verschachtelungen auftritt? Mit der Behandlung des Fehlers auf jeder Stufe wird der Code noch schlechter lesbar. Häufig gelingt das auch nicht korrekt, und das Programm stürzt einfach ab, wenn tief unten in der Verschachtelung ein Fehler auftritt.

Die aktuelle JavaScript Version, die von neueren Chrome- und Firefox Browsern auch schon unterstützt wird, bringt Abhilfe. Zuerst noch zur Klärung: Diese JavaScript Version hört auf den Namen ES 6, aber auch auf den Namen ES 2015, ECMA6 und ECMA 2015, was manchmal ein wenig verwirrlich ist.

Wie auch immer. Die Idee ist: Anstatt die länger dauernde Aufgabe zu lösen, und dann ein Callback mit der Lösung aufzurufen, liefern wir sofort ein Objekt zurück, welches nicht die Lösung ist, sondern welches verspricht, die Lösung irgendwann zu liefern.

      Hauptprogramm(){
        fetchABC().then(coolData=>{
            display(coolData)
          }).catch(error=>{
            display("oops: "+error)
            })
      }

fetchABC() hat eine Promise zurückgeliefert (wir kommen weiter unten darauf, wie das geht), und der Programmteil nach “then” ist die Einlösung des Versprechens. Elegant ist der “catch”-Teil: Wenn das Versprechen nicht erfüllt werden konnte (ja, liebe Kinder, Versprechen werden manchmal gebrochen!), dann wird dieser Teil ausgeführt. Das Schöne ist, egal in welcher Verschachtelungstiefe dir Ursache war, das Programm gelangt immer an diese Stelle zur Fehlerbehandlung.

Wie wurde nun also diese Promise hergestellt? Wir brauchen drei Datenelemente A, B und C, welche von verschiedenen langsamen, aber voneinander unabhängigen Quellen stammen. Da die Quellen unabhängig sind, können wir alle drei Abfragen quasi-gleichzeitig abschicken, wenn wir nur sicherstellen, dass wir alle drei Resultate haben, bevor wir weiter machen. Das war mit den bisher gezeigten Techniken nicht so einfach zu machen. Jetzt ist es fast trivial:

      function fetchABC(){
        promiseA=fetchA()
        promiseB=fetchB()
        promiseC=fetchC()
        return Promise.all([promiseA,promiseB,promiseC]).then(resolve=>{
          /* Allfällig Verarbeitungsschritte */
          })
      }

Die Funktion gibt eine Promise zurück, welche erst dann eingelöst wird, wenn alle drei Vorbedingungs-Promises erfüllt sind.
Das ist alles.

Wirklich schön wird es nun mit dem nächsten Schritt, der im Grund nur eine Syntaxänderung ist, aber asynchrone Programmierung gleich einfach macht, wie synchrone Programmierung:

      async function fetch_and_show(){
        let coolData=await fetchABC()
        display(coolData)
      }

Hinter den Kulissen wird hier immer noch mit Promises hantiert, aber die ganzen komplizierten Konstrukte sind aus dem Quellcode verschwunden. Die Schlüsselwörter async und await können mit Typescript ab Version 2 und mit JavaScript ab Version ECMA6 eingesetzt werden.

Ach ja, aufmerksame Leser/innen werden vielleicht monieren, dass ich eigentlich immer noch nicht gezeigt habe, wie man eine Promise herstellt, zum Beispiel die oben erwähnte PromiseA.

Man kann das auf drei verschiedene Arten tun:

1.: Trivial:

Die Funktion ruft ihrerseits eine Funktion auf, die eine Promise zurückliefert, zum Beispiel eine Funktion eines asynchrones, Promise-enabled Datenbank- oder HTTP-Treibers.

2.: Promise explizit erstellen:

    function fetchA(){
        return new Promise((resolve,reject)=>{
            let result=getTheDataFromRemoteWhichtakesSomeTime()
            if(allesHatGeklappt){
                resolve(result)
            }else{
                reject(new Error("Ups, da ging was schief")
            }
        })
    }

Wir erzeugen eine neue Promise, die Methoden für die Erfüllung oder den Bruch des Versprechens enthält und liefern diese Promise sofort zurück.

3.: Promise implizit erstellen

    async function fetchA(){
        return getTheDataFromRemoteWhichtakesSomeTime()
    }

Jede mit “async” gekennzeichnete Funktion liefert implizit eine Promise zurück. Die Promise hat -ebenfalls implizit- einen resolve-Zweig, der den deklarierten return-Wert der Funktion liefert, und einen reject-Zweig, der bei jeder während der Ausführung geworfenen Exception mit diesem Exception Object aufgerufen wird.