Bertl: Async Await und .then()?

Halli und o,

Ich hätte da eine Frage bezüglich Asynchronous Javascript...

async function warten() {
	await new Promise((resolve, reject) => {
		setTimeout(resolve, 1000);
	});
}

// ICH FUNKTIONIERE!
async function trigger() {
	await warten();
	console.log("GEWARTET!");
}

trigger();

Funktioniert problemlos, nach 1000ms wird das Promise resolved und in trigger() das console.log("GEWARTET!") ausgelöst.

Nicht so, wenn ich ein chaining mit .then vornehme, nach dem Schema

// …ICH ABER NICHT!
async function trigger() {
	await warten().then(console.log("WARUM WARTE ICH NICHT?"));
}

Warum wird HIER das console.log nun sofort ausgelöst?

[Habe auch schon mit async und await herumgespielt, ob ich die Keywörter jetzt entferne oder nicht, macht da keinen großen Unterschied]

Dank euch für Aufklärung,

LG, Bertl

  1. Hallo Bertl,

    da sind eine ganze Menge Fehler drin, die in ihrer Boshaftigkeit Sauron Konkurrenz machen. Großartiges Anschauungsmaterial!

    Grundsätzlich: .then zu verwenden statt await ist eine gute Idee, wenn Du keine async-Triggerfunktion schreiben willst. Du hast aber auch eine Alternative. Deklariere dein Script als type="module". In einem ECMAScript-Modul darfst Du auch auf der Top-Ebene await verwenden. Beachte vom Timing her, dass Scripte mit type="module" als defer-Scripte behandelt werden, d.h. wenn das Script im head steht, wird es nicht wie sonst vor dem Body ausgeführt, sondern am Schluss, gleich vor dem DOMContentLoaded Event. Leider kann ich Dich für ECMAScript-Module nicht an unser Wiki verweisen, es hat dort eine Baustelle wo gerade der erste Spatenstich gemacht ist.

    Nun zu deiner Frage:

    Eine async-Funktion liefert ein Promise auf ihr Ergebnis. Heißt:

    let www = warten();
    

    ergibt ein Promise. Dieses kannst Du awaiten oder mit .then() und .catch() Callbacks für fulfilled oder rejected registrieren.

    Und

    let www = await warten();
    

    liefert den Wert, den Du aus warten mit return zurück gibst. Hä? Ja genau. Du gibst keinen zurück. In www würde also undefined stehen.

    Man müsste also annehmen, dass

    await warten().then(console.log("WARUM WARTE ICH NICHT?"));
    

    sich mit einer Fehlermeldung wie "Cannot read properties of undefined (reading 'then')" erbricht. Tut es aber nicht. Und warum? Der Punkt hat höhere Priorität als await, d.h. er ruft das .then() auf dem Promise auf, das warten() zurückgibt und awaitet dann das Promise, das .then() zurückgibt. Und weil aus then immer ein Promise zurückkommt, gibt das keinen Error.

    Und warum wartet er nicht?

    then erwartet zwei Parameter. Der erste ist ein Callback für den Fall, dass das Promise erfüllt wird und der zweite ist ein Callback für den Fall, dass das Promise zurückgewiesen wird. Beide sind optional, d.h. wenn einer der beiden Parameter undefined ist, wird er kommentarlos ignoriert.

    console.log("Huhu") ist aber kein Callback und liefert auch keinen Callback, sondern undefined. Statt dessen wird die log-Methode direkt vor dem Aufruf von then ausgeführt, um ihren Rückgabewert an then zu übergeben. Der ist undefined und wird von then ignoriert, statt einen Fehler wie "Ich will aber einen Callback!" zu erzeugen.

    Also:

    await warten().then(console.log("WARUM WARTE ICH NICHT?"));
    

    (1) kein await, weil Du das Promise aus warten haben willst
    (2) console.log nicht direkt aufrufen, sondern einen Callback übergeben, der loggt

    console.log("Go");
    warten().then(() => console.log("WARUM GEHT DAS NICHT SCHNELLER?"));
    

    Rolf

    --
    sumpsi - posui - obstruxi
    1. Hola die Waldfee

      Danke für den Input!

      ...aber ist das denn schlimm, wenn ein Promise nichts zurückgibt?

      Oder anders geschrieben, wo bestehen jetzt die schlimmen Fehler?

      Will mit den Promises eigentlich keine Daten irgendwo fetchen sondern sie eher mit DOM Events verknüpfen (beispielsweise "animationend", etc.)


      Hintergrund dieser Vorgangsweise ist, dass ich den Code dann schöner "inline" nacheinander schreiben kann, nach dem Schema

      // Javascript Code
      // await DOM animation end
      // weiterer Javascript Code
      

      Klingt zwar lächerlich, aber behalte mal den Überblick über alle durch EventListener ausgelöste Callback Funktionen, wenn fünf CSS Animationen hintereinander ausgeführt werden sollen und danach der Javascript Code weiter ausgeführt werden soll, da ist

      // Javascript Code
      // await DOManimation1  end
      // Javascript Code nach erster DOM animation
      // await DOManimation2  end
      // await DOManimation3  end
      // Javascript Code nach DOM animations
      

      schon viel übersichtlicher und praktischer.

      1. Hallo,

        ...aber ist das denn schlimm, wenn ein Promise nichts zurückgibt?

        ich kann zu modernen Javascript- oder ECMAScript-Features nicht viel Konkretes sagen; meine Kenntnisse sind seit zehn Jahren oder so eingestaubt. Aber aus einer Funktion "nichts" als Ergebnis zu liefern, wenn die übergeordnete Logik eigentlich ein Ergebnis erwartet und damit arbeiten will, ist schon ein kapitaler Fehler. Das fliegt einem früher oder später um die Ohren.

        Will mit den Promises eigentlich keine Daten irgendwo fetchen sondern sie eher mit DOM Events verknüpfen (beispielsweise "animationend", etc.)

        Ja, okay. Aber der Sinn von Promises ist doch, dass eine Funktion dem Aufrufer signalisieren kann: Ja, du bekommst ein Ergebnis, aber nicht sofort. Ich melde mich wieder. Etwa so, als ob du deinen Sohn zum Spielen nach draußen schickst und ihm aufträgst: Wenn du wieder reinkommst, denk dran, dass du noch sechs Brötchen vom Bäcker mitbringst. Dann ist deine Erwartungshaltung, dass der Bub irgendwann mit einer Tüte Brötchen zurückkommt. Bringt er die nicht mit, ist der Plan fürs Abendessen geplatzt.

        Klingt zwar lächerlich, aber behalte mal den Überblick über alle durch EventListener ausgelöste Callback Funktionen, wenn fünf CSS Animationen hintereinander ausgeführt werden sollen und danach der Javascript Code weiter ausgeführt werden soll

        Ja, gut lesbarer und verständlicher Code ist Gold wert. Aber überlege dabei immer auch: Würde jemand anders den Code auch verstehen? Oder du selbst, wenn du das Projekt zwei Jahre später mal wieder anfassen musst?

        Einen schönen Tag noch
         Martin

        --
        Kundin zur Verkäuferin im Modegeschäft: "Sagen Sie, haben Sie auch Sachen von der Marke SALE? Die sind immer so schön günstig!"
        1. Hallo, danke an Martin und Rolf!

          @Der Martin

          Ja, gut lesbarer und verständlicher Code ist Gold wert. Aber überlege dabei immer auch: Würde jemand anders den Code auch verstehen? Oder du selbst, wenn du das Projekt zwei Jahre später mal wieder anfassen musst?

          Genau darum geht's auch irgendwie...

          Nur mit "animationend" Listenern passiert eine ziemlich hässliche Callback Verschachtelung mit einer ungewollt erzwungenen Filetierung von vielleicht zusammengehörigem Code, nach dem Schema

          // MMMMMMMMMH - CODESALAT! :P
          
          function ueberDrueberFunction() {
          
          	function ersteFunktion() {
          		erstesElement.classList.remove("animation");
          		zweitesElement.addEventListener("animationend", zweiteFunktion);
          		zweitesElement.classList.add("animation");
          		// DO OTHER STUFF 1 ONLY WHEN FINISHED
          	}
          
          	function zweiteFunktion() {
          		zweitesElement.classList.remove("animation");
          		drittesElement.addEventListener("animationend", dritteFunktion);
          		drittesElement.classList.add("animation");
          		// DO OTHER STUFF 2 ONLY WHEN FINISHED
          	}
          
          	function dritteFunktion() {...}
          
          	erstesElement.addEventListener("animationend", ersteFunktion);
          	erstesElement.classList.add("animation");
          	
          }
          

          (Ja, könnte man vielleicht mit bind refactoren, geschenkt)

          ...wohingegen mein Schema doch recht übersichtlich und gut leserlich bleibt (wenn man animationend mit einer Resolve Rückgabe verknüpft):

          async function functionMitPromiseWennAnimationEnde(args) {
          	...
          }
          
          async function ueberDrueberFunction() {
          	await functionMitPromiseWennAnimationEnde({element: erstesElement, animation: "animation"});
          	// DO OTHER STUFF 1
          	await functionMitPromiseWennAnimationEnde({element: zweitesElement, animation: "animation"});
          	// DO OTHER STUFF 2
          	await functionMitPromiseWennAnimationEnde({element: drittesElement, animation: "animationAnders"});
          	// DO OTHER STUFF 3
          }
          
          1. Hallo Bertl,

            das klingt erstmal vielversprechend, du musst nur schauen, ob Du die Eventlistener auch wieder entfernen musst. Das ist nicht nötig, wenn die Elemente nach Abschluss der Animation aus dem DOM entfernt werden, oder Du sicher bist, dass die Animation nur einmal spielt. Aber wenn sie ggf. mehrfach ablaufen kann, hättest Du ohne removeEventListener nachher mehrere Listener laufen.

            Da Du auf ein animationend nur einmal reagieren musst, bietet sich hier die once Option von addEventListener an:

            async function functionMitPromiseWennAnimationEnde(elem, class) {
               return new Promise((resolve,reject) => {
                  elem.classList.add(class);
                  elem.addEventListener(
                     "animationend",
                     function() {
                        elem.classList.remove(class);
                        resolve();
                     },
                     { once: true });
            
               });
            }
            

            Rolf

            --
            sumpsi - posui - obstruxi
            1. Da Du auf ein animationend nur einmal reagieren musst, bietet sich hier die once Option von addEventListener an:

              F*** tatsächlich! (Heißt, mit ONCE wird dann der Listener AUTOMATISCH wieder gelöscht...?)

              Danke ROLF!

      2. Hallo Bertl,

        wenn fünf CSS Animationen hintereinander ausgeführt werden sollen und danach der Javascript Code weiter ausgeführt werden soll

        Ob Du das mit Promises hinbekommst, da zweifle ich noch. Dazu müsste man mehr über den Plan wissen, den Du verfolgst.

        aber ist das denn schlimm, wenn ein Promise nichts zurückgibt?

        Nö. Das ist nicht schlimm.

        Ich war da, scheint mir, selbst ein bisschen verwirrt - denn dass await warten() zu undefined führt hat mit dem Rest nicht so viel zu tun.

        Entscheidender ist, dass man await und then besser nicht mischt. Wenn Du 3 Animationsfunktionen hast, die jeweils ein Promise resolven, sobald die Animation durch ist, dann mach entweder

        function animate()
        {
           return animation1()
                  .then(animation2)
                  .then(animation3);
                  .then(() => console.log("fertig");
           // das Promise des letzen .then wird zurückgegeben. Es resolved
           // zu undefined (weil console.log das zurückgibt)
        }
        

        ODER

        async function animate()
        {
           await animation1();
           await animation2();
           await animation2();
           console.log("fertig");
           // Ein Promise wird zurückgegeben, das zu undefined resolved,
           // sobald die animate-Funktion durch ist.
        }
        

        Du kannst auf beide Varianten mit await warten oder auf den Rückgabewert mit .then einen Fortsetzungshandler registrieren.

        Ob sich das für Animationen eignet, musst Du schauen. Das hängt auch davon ab, was deine Animierfunktionen tun. Normalerweise schreibt man Animationen über Events (requestAnimationFrame) oder man definiert sie mit CSS. Es funktioniert nicht, in einer for-Schleife eine Variable von 0 bis 100 laufen zu lassen um bspw. ein Rechteck von Position 0 bis 100 zu bewegen. Weil dazwischen immer eine Layout-Phase nötig ist, und diese nicht kommt, solange JavaScript läuft.

        Statt dessen gibt es das WAAPI (Web Animations API) und auch CSS Animationen, beide mit eigenen Events und Schnittstellen. Ich habe damit noch nicht so viel gemacht und kann darum nicht viel dazu beitragen.

        Rolf

        --
        sumpsi - posui - obstruxi