Herr Bert: async await Chaos

Hallo,

Ich habe eine etwas komplexere Ausgangssituation, die sowohl fetch, als auch ein Durchreichen eines Parameters (bzw. eigentlich eines Arguments) erfordert.

Hab mein Problem soweit es geht simplifiziert, denn wer nicht simplifiziert, verliert.

Im Endeffekt habe ich folgenden Code:

const main = (async () => {
	const endergebnis = await [fetchNmutate1, fetchNmutate2].reduce(async (acc, cur) => {
		const zwischenergebnis = await cur(acc);
		return zwischenergebnis;
	}, 0); // Ergebnis hier: NaN // ?
})();

fetchNmutate1 und fetchNmutate2 sind dabei Funktionen, die Data fetchen und dann das Ausgangsargument (hier 0) behandeln.

Aber selbst wenn ich die fetch Funktionalität weglasse, und die beiden Funktionen fetchNmutate1 und fetchNmutate2 z.B. auf eine simple Additionen oder Multiplikation (etc. ) beschränke (und das Resultat natürlich returne), lautet das Endresultat immer NaN.

Warum?

Danke, Herr Bert

  1. Hallo,

    Hi

    Ich habe eine etwas komplexere Ausgangssituation, die sowohl fetch, als auch ein Durchreichen eines Parameters (bzw. eigentlich eines Arguments) erfordert.

    Hab mein Problem soweit es geht simplifiziert, denn wer nicht simplifiziert, verliert.

    Aha.

    Im Endeffekt habe ich folgenden Code:

    const main = (async () => {
    	const endergebnis = await [fetchNmutate1, fetchNmutate2].reduce(async (acc, cur) => {
    		const zwischenergebnis = await cur(acc);
    		return zwischenergebnis;
    	}, 0); // Ergebnis hier: NaN // ?
    })();
    
    

    Aber selbst wenn ich die fetch Funktionalität weglasse, und die beiden Funktionen fetchNmutate1 und fetchNmutate2 z.B. auf eine simple Additionen oder Multiplikation (etc. ) beschränke (und das Resultat natürlich returne), lautet das Endresultat immer NaN.

    Warum?

    Was wäre, wenn du zum Beispiel versuchst, 25 durch „Banane“ zu teilen? Es macht nicht viel Sinn, oder? Aber, 25 geteilt durch Banane ist NaN. Daher ist im Grunde, das was wegsimplifiziert wurde, vermutlich Fehlerquelle.

    Vielleicht hilft das weiter: Es gibt fünf Haupttypen von Fehlern, die NaN als Ergebnis zurückgegeben werden:

    • Wenn die Zahl nicht geparst werden kann (parseInt("text"));
    • Jede mathematische Operation, bei der das Ergebnis keine reelle Zahl ist (Math.sqrt(-1));
    • Verwendung von NaN in der mathematischen Operation als Operand;
    • Verwenden der unbestimmten Formen (undefined+ undefined, Beispiel mit infinity geteilt durch infinity usw.);
    • Einbeziehung der Zeichenfolge als Element einer mathematischen Operation

    Grütze

  2. Hallo Herr Bert,

    das liegt daran, dass reduce nicht darauf ausgelegt ist, mit Promises zu arbeiten.

    Dein übriger Code ist übrigens merkwürdig - warum erzeugst Du eine Konstante main, der Du eine Pfeilfunktion zuweist, die Du als IIFE verwendest? Die Pfeilfunktion gibt nichts zurück, in main steht nachher also undefined. Möglicherweise ist das in deinem eigenen Code umfangreicher und ergibt mehr Sinn, aber wenn Du nichts weiter willst, als asynchrone Funktionen awaiten zu können, dann lass das. Gib deinem Script type="module" und gut ist. Der IE ist tot, man kann ECMAScript-Module ohne Bedenken verwenden. In denen ist ein await auf Top-Ebene erlaubt.

    Aber zu deinem Problem.

    Du hast ein Array von zwei Funktionen und setzt reduce darauf an. Diese Funktionen sind offenbar async-Funktionen, denn Du awaitest ihren Rückgabewert.

    reduce ruft den Callback für fetchNMutate1 auf und übergibt die Werte 0 und fetchNMutate1.

    Im Callback rufst Du fetchNMutate1 auf und übergibst die 0. Das Ergebnis von fetchNMutate1 erwartest Du mit await - und damit das möglich ist, machst Du auch den ganzen Callback async.

    Aber was passiert nun? Eine async-Methode gibt nicht den Wert zurück, den Du mit return zurückgibst, sondern ein Promise, dass Du awaiten kannst und von dem Du dann den Wert bekommst.

    Heißt also: reduce schert sich nicht um dein await im ersten Callback, sondern bekommt das Promise deines asynchronen Callbacks.

    Der zweite Callback wird demnach mit acc=[Promise] und cur=fetchNMutate2 aufgerufen.

    Dieses Promise übergibst Du an fetchNMutate2, wo aber eigentlich ein Akkumulatorwert erwartet wird.

    Du musst also das Promise, dass Du in acc bekommst, erstmal awaiten, und deswegen auch für den Startwert ein Promise übergeben.

    <script type="module">
      const funcs = [fetchNmutate1, fetchNmutate2];
      const endergebnis = await funcs.reduce(async (accP, cur) => {
        const acc = await accP;
        const zwischenergebnis = await cur(acc);
        return zwischenergebnis;
      }, Promise.resolve(0));
    </script>
    

    Scheußlich. Wirklich scheußlich. Gelle?

    Ich hätte da was für Dich:

    <script type="module">
      const funcs = [fetchNmutate1, fetchNmutate2];
      let acc = 0;
      for (let func of funcs) {
         acc = await func(acc);
      }
    </script>
    

    oder als async-Funktion mit Array-Parameter für die Funktionen

    <script type="module">
      async function collectFetchesAsync(funcArray) {
        let acc = 0;
        for (let func of funcArray) {
           acc = await func(acc);
        }
        return acc;
      }
    
      const ergebnis = await collectFetchesAsync( [fetchNmutate1, fetchNmutate2] );
    </script>
    

    oder als async-Funktion mit Rest-Parameter

    <script type="module">
      async function collectFetchesAsync(...funcs) {
        let acc = 0;
        for (let func of funcs) {
           acc = await func(acc);
        }
        return acc;
      }
    
      const ergebnis = collectFetchesAsync(fetchNmutate1, fetchNmutate2);
    </script>
    

    Besser? Array.prototype.reduce ist LÄNGST nicht immer die beste Wahl.

    Rolf

    --
    sumpsi - posui - obstruxi
    1. WOW, danke Rolf für die exzellente Anleitung! :O

      Echt top, wie viel Zeit hier manche in ihnen völlig unbekannte Individuen stecken! (Rechnete eher mit einer Reaktion der Marke 'Geh sterben...')

      ...und daneben eine ganz neue Erkenntnis eingesackt - wusste nicht das Module Top-Level awaits zulassen (meiner Meinung nach LAAAANGE überfällig...)!

      Wichtigster Erkenntnisgewinn für mich war hier wohl, dass eine async Methode AUCH in der reduce Schleife ein Promise zurückgibt, das ich awaiten muss (EIGENTLICH logisch).

      Wird aber ein Promise nicht sofort resolved, wenn es nichts zu resolven gibt? In anderen Worten: muss ich als Startwert tatsächlich Promise.resolve(0) übergeben, oder wäre nicht auch einfach 0 gangbar?

      Dann würde die nächste Iteration const acc = await accP; sozusagen als const acc = await 0; ausgerufen, was sofort resolved werden sollte und das Loop unbeeindruckt weiterlaufen sollte.

      ...OOODER übersehe ich da schon wieder irgendwelche Caveats?

      1. Hallo Herr Bert,

        Echt top, wie viel Zeit hier manche in ihnen völlig unbekannte Individuen stecken!

        Ja, ich muss schon verrückt sein 😉

        Aber tatsächlich lerne ich durch das Analysieren der Probleme anderer auch selbst immer wieder dazu. Ganz uneigennützig ist das also nicht. Und es tut ab und zu auch mal gut, wenn einem jemand zuhört.

        Die Motivation der anderen Foristen mag eine andere sein. Einige sind Lehrer, vermutlich tut's auch denen gut, wenn zur Abwechslung mal jemand zuhört…

        muss ich als Startwert tatsächlich Promise.resolve(0) übergeben, oder wäre nicht auch einfach 0 gangbar?

        Das Problem ist, dass dein async-Callback auf jeden Fall ein Promise zurückgibt. Weil er async ist. Und weil der Rückgabewert des reduce-Callbacks der neue acc Wert ist, kommt spätestens ab dem zweiten Aufruf des Callbacks ein Promise als acc Wert an und wenn Du das nicht awaitest, kommst Du an seinen Wert nicht 'ran.

        Wegen des await auf den acc Wert musst Du auch als Initialwert ein Promise hineingeben. Weil Du etwas anderes nicht awaiten kannst. Oder möchtest Du im Callback etwa abfragen, ob acc ein Promise ist oder nicht? Dann doch lieber den Startwert in ein Promise verpacken.~~

        Und nun habe ich etwas gelernt. Man kann nicht nur Promises awaiten. Jeder andere Wert wird einfach durch den await durchgereicht. Ts.

        Ich habe noch mehr gelernt. Der Wert wird nicht durch den await durchgereicht. JavaScript verpackt ihn ihn ein Promise, damit auf jeden Fall das await-Timing stattfindet. Du kannst also als Initialwert 0 übergeben. JavaScript macht in dem Moment, wo Du 0 awaiten willst, automagisch ein Promise.resolve(0) daraus.

        Wird aber ein Promise nicht sofort resolved, wenn es nichts zu resolven gibt?

        Doch, wird es. Aber du kannst auch Promises awaiten, die schon erfüllt sind. Es ist auch der einzige Weg, um an ihren Wert heranzukommen. Was nicht heißt, dass dieser await NICHT wartet. Was hinter await steht, wird immre im then-Callback des awaiteten Promise ausgeführt und damit in die Microtask-Queue verlagert.

        Was irgendwie beeindruckend ist. Die reduce-Methode weiß überhaupt gar nichts von all dem. D.h. sie läuft blindlings durch und erzeugt einen Riesenschwanz an Promises, die sich erst danach Stück für Stück erfüllen.

        async function test() {
          let numbers = [ 1, 2, 3, 4, 5, 6 ];
        
          let pSum = numbers.reduce( async (pAcc, curr) => {
                        console.log("will reduce " + curr);
                        let acc = await pAcc;
                        let newAcc = await computeAsync(acc, curr);
                        console.log("reduce " + curr + " - newAcc is " + newAcc);
                        return newAcc;
                     });
        
          console.log("reduce done");
        
          let sum = await pSum;
          console.log(sum);
        }
        
        test();
        console.log("Test done");
        

        Überleg mal, in welcher Reihenfolge die Ausgaben wohl kommen. Ohne auszuprobieren 😉

        Und du weißt ja, dass ein await nichts weiter ist als Syntaxzucker für einen then-Callback?

        console.log("Vorher");
        let a = await doAsync(234);
        console.log("Nachher: ", a);
        

        ist EXAKT das gleiche wie

        console.log("Vorher");
        doAsync(234)
        .then(a => {
           console.log("Nachher: ", a);
        });
        

        Aufgabe für schlaflose Nächte: Schreibe dein await-Zeug in die zuckerfreie Variante mit .then um. Aber spätestens dann wirst Du einsehen, warum ich diese await-Orgie "scheußlich" nannte.

        Rolf

        --
        sumpsi - posui - obstruxi
        1. Hinweis: Dieser Beitrag wurde ca drölf mal editiert... Du hast also ggf. nicht den letzten Stand gelesen. Aber jetzt bin ich fertig 😉