Eine knifflige Falle von Promise.all() und eine Lösung

Ziel dieses Artikels ist es, eine schwierige Situation zu beschreiben, die bei der Verwendung von Promise.all() häufig auftreten kann, und eine einfache Lösung für dieses Problem zu finden. Der Kontext Die MDN-Seite für Promise.all() bietet die folgende Beschreibung dieser Funktion: Die statische Methode Promise.all() nimmt eine Iterable von Versprechen als Eingabe …

Ziel dieses Artikels ist es, eine knifflige Situation zu beschreiben, die bei der Verwendung häufig auftreten kann Promise.all()und eine einfache Lösung für dieses Problem.

Der Kontext

Die MDN-Seite für Promise.all() bietet die folgende Beschreibung dieser Funktion:

Der Promise.all() Die statische Methode verwendet eine Iteration von Versprechen als Eingabe und gibt eine einzelne zurück Versprechen. Dieses zurückgegebene Versprechen wird erfüllt, wenn alle Versprechen der Eingabe erfüllt sind (einschließlich der Übergabe eines leeren Iterables), mit einem Array der Erfüllungswerte. Es lehnt ab, wenn eines der Versprechen der Eingabe abgelehnt wird, mit diesem ersten Ablehnungsgrund.

Promise.all() wurde in der 6. Ausgabe von ECMAScript eingeführt, die 2015 veröffentlicht wurde, als Versprechen erstmals als integrierter Mechanismus in die Sprache hinzugefügt wurden, aber sie wurden bereits zuvor durch die Verwendung von Bibliotheken wie Bluebird weit verbreitet.

ECMAScript 2020, die 11. Ausgabe, vorgestellt Promise.allSettled()das sich etwas anders verhält:

Der Promise.allSettled() Die statische Methode verwendet eine Iteration von Versprechen als Eingabe und gibt eine einzelne zurück Versprechen. Dieses zurückgegebene Versprechen wird erfüllt, wenn alle Versprechen der Eingabe erfüllt sind (einschließlich der Übergabe eines leeren Iterables), mit einem Array von Objekten, die das Ergebnis jedes Versprechens beschreiben.

Schließlich mit dem Aufkommen von async, Promise.all() & Promise.allSettled() kann benutzt werden um await zur Vervollständigung mehrerer asynchroner Funktionen:

await Promise.all([asyncFunc1(), asyncFunc2()]) // can throw an exception

const results = await Promise.allSettled([asyncFunc1(), asyncFunc2()]) // never throw an exception

Das Problem

Promise.all() ist eine sehr praktische Funktion, aber ihr Verhalten hinsichtlich der Fehlerverwaltung ist eigentlich ziemlich knifflig.

Lesen Sie auch  Die TV-Design-Philosophie der Eliminierung – Samsung Global Newsroom

Wohingegen Promise.allSettled() Es ist erforderlich, die resultierenden Status des Versprechens und das von ihm erstellte Versprechen zu überprüfen Promise.all() Wille ablehnen wenn eines der Eingabeversprechen fehlschlägt. Und in diesem Fall bedeutet es das await Promise.all(...) wird eine Ausnahme auslösen.

Aber Wann Promise.all() lehnt ab, einige der Eingabeversprechen können noch ausgeführt werden!
Und diese Situation wird von Entwicklern oft nicht berücksichtigt.

Das folgende Diagramm und Codeausschnitt veranschaulichen dieses Problem:

async function sleep(durationMs) {
    return new Promise((resolve) => setTimeout(resolve, durationMs));
}

async function sleepAndFail(durationMs) {
  await sleep(durationMs);
  throw new Error(`sleepAndFail(${durationMs}) FAILED`);
};

let promiseStatus = "not-started";
async function sleepAndTrackStatus(durationMs) {
  promiseStatus = "executing";
  await sleep(durationMs);
  promiseStatus = `sleepAndTrackStatus(${durationMs}) SUCCEEDED`;
};

(async function () {
  const failing = sleepAndFail(100);
  const fastSucceeding = sleep(50);
  const slowSucceeding = sleepAndTrackStatus(200);
  try {
    console.log(await Promise.all([
      failing,
      fastSucceeding,
      slowSucceeding,
    ]));
  } catch (error) {
    console.log(error); // -> Error: sleepAndFail(100) FAILED
  }
  console.log("Final promiseStatus for slowSucceeding:", promiseStatus); // -> executing!
})()

Das Problem besteht darin, dass, wenn ein Eingabeversprechen fehlschlägt, Promise.all() Wille frühzeitig ablehnenohne auf die anderen Versprechen zu warten, die weiterhin asynchron verarbeitet werden können.

Dies kann zu vielen problematischen Situationen führen, in denen Code, der von diesen anderen Versprechen ausgeführt wird, Vorgänge ausführt, die mit dem Code, der nach dem Aufruf von ausgeführt wird, in Konflikt geraten können Promise.all(). Beispielsweise ist mit Problemen bei Datenbankverbindungen oder I/O mit Dateien zu rechnen.

In meinem Fall habe ich dieses zugrunde liegende Problem in einer Situation kaskadierender Fehler bei einer Jest-Testsuite identifiziert: Der Code war teilweise asynchron afterEach() Die Methoden warteten nicht ordnungsgemäß auf die Bereinigung freigegebener Ressourcen, wenn einige Komponententests aufgrund eines Aufrufs von fehlschlugen await Promise.all().

Die Grundursache

Aus Neugier habe ich versucht, den Quellcode der Implementierung von herauszufinden Promise.all(). Ich denke, der größte Teil seiner Logik liegt in der Datei src/builtins/promise-all.tq in der V8-JavaScript-Engine.
.tq Dateien werden in Torque geschrieben, einer für V8 spezifischen Hochsprache, die in C++ transpiliert wird. Daher fiel es mir schwer herauszufinden, wo genau in der Implementierung die „Kurzschluss“-Versprechensablehnungslogik passiert 😅.

Lesen Sie auch  Für die jüdischen Sicherheitskräfte von Los Angeles war der israelische Angriff ein Aufruf zu den Waffen

Für den neugierigen Leser gibt es einen Abschnitt über die Torque-Sprache im hervorragenden Learning-v8-Repo von Daniel Bevenius.

Eine Lösung

Zu diesem Problem: Promise.allSettled() ist eine bessere Alternative zu Promise.all(), während es auf die Erfüllung aller Versprechen wartet, die als Parameter an diese Funktion übergeben wurden. Tatsächlich, allSettled wurde genau aus diesem Grund in EcmaScript eingeführt, wie aus dem ursprünglichen Vorschlagsdokument hervorgeht:

Promise.allSettled ist einzigartig, da es immer auf alle seine Eingabewerte wartet.

Jedoch, Promise.allSettled() erfordert, dass Sie potenzielle Ablehnungen, die leicht vergessen werden können, selbst bearbeiten oder wiederholten Standardcode erstellen.

Die Lösung, die ich vorschlage, ist ein sicherer Ersatz für Promise.all():

/*
 * This function ensures that (1) all the promises provided have completed
 *                        and (2) that a rejection is produced if at least one of those promises is rejected.
 */
async function waitForPromises<T>(promises: Iterable<PromiseLike<T>>) {
  const results = await Promise.allSettled(promises);
  const rejectedResults: PromiseRejectedResult[] = results.filter(
    (result): result is PromiseRejectedResult => result.status === "rejected"
  );
  if (rejectedResults.length === 1) {
    throw rejectedResults[0].reason;
  }
  if (rejectedResults.length > 1) {
    throw new AggregateError(rejectedResults.map((result) => result.reason), `${rejectedResults.length} promises failed`);
  }
  const successfullResults: PromiseFulfilledResult<Awaited<T>>[] = results.filter(
    (result): result is PromiseFulfilledResult<Awaited<T>> => result.status === "fulfilled"
  );
  return successfullResults.map((result) => result.value);
}

Dies ist tatsächlich TypeScript mit Generics, aber Sie können das einfach entfernen : types Und um gültigen Javascript-Code zu erhalten:

waitForPromises in Javascript
async function waitForPromises(promises) {
  const results = await Promise.allSettled(promises);
  const rejectedResults = results.filter(result => result.status === "rejected");
  if (rejectedResults.length === 1) {
    throw rejectedResults[0].reason;
  }
  if (rejectedResults.length > 1) {
    throw new AggregateError(rejectedResults.map((result) => result.reason), `${rejectedResults.length} promises failed`);
  }
  return results.map((result) => result.value);
}

Sie können diese Funktion durch Ersetzen testen Promise.all von waitForPromises im ersten Codeausschnitt dieses Artikels.

Obwohl manchmal das „Kurzschluss“-Verhalten von Promise.all kann praktisch sein, denke ich waitForPromises ist in den meisten Situationen eine bessere und sicherere Alternative und sollte die Standardoption sein await die Ausführung mehrerer asynchroner Funktionen.

(Danke an Reddit-Benutzer @senocular für die sehr relevanten Rückmeldungen zu diesem Blogbeitrag)

Weitere Lektüre

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.