日本語版
最新ニュース
科学&テクノロジー

Promise.all() の厄介な落とし穴とその解決策

この記事は、Promise.all() を使用するときによく発生する可能性のある厄介な状況と、この問題の簡単な解決策について説明することを目的としています。 コンテキスト Promise.all() の MDN ページでは、この関数について次の説明が提供されています。 Promise.all() 静的メソッドは入力として Promise の反復可能を受け取ります。この記事は、使用時によく発生する可能性のある厄介な状況について説明することを目的としています。 Promise.all()、そしてこの問題の簡単な解決策。 コンテキスト MDN ページ Promise.all() では、この関数について次のように説明します。 の Promise.all() 静的メソッドは入力としてプロミスの反復可能値を受け取り、単一のプロミスを返します。 約束。 この返された Promise は、入力のすべての Promise が満たされたとき (空の反復可能値が渡されたときを含む)、満たされた値の配列を使用して満たされます。 入力のいずれかの Promise が拒否されると、この最初の拒否理由で拒否されます。 Promise.all() 2015 年に発行された ECMAScript の第 6 版で導入されました。、Promise が最初に言語の組み込みメカニズムとして追加されたとき、しかし、それらは以前から、次のようなライブラリの使用を通じてすでに広く使用されていました。…

Promise.all() の厄介な落とし穴とその解決策

1715089072
2024-05-06 12:15:05

この記事は、Promise.all() を使用するときによく発生する可能性のある厄介な状況と、この問題の簡単な解決策について説明することを目的としています。 コンテキスト Promise.all() の MDN ページでは、この関数について次の説明が提供されています。 Promise.all() 静的メソッドは入力として Promise の反復可能を受け取ります。

この記事は、使用時によく発生する可能性のある厄介な状況について説明することを目的としています。 Promise.all()、そしてこの問題の簡単な解決策。

コンテキスト

MDN ページ Promise.all() では、この関数について次のように説明します。

Promise.all() 静的メソッドは入力としてプロミスの反復可能値を受け取り、単一のプロミスを返します。 約束。 この返された Promise は、入力のすべての Promise が満たされたとき (空の反復可能値が渡されたときを含む)、満たされた値の配列を使用して満たされます。 入力のいずれかの Promise が拒否されると、この最初の拒否理由で拒否されます。

Promise.all() 2015 年に発行された ECMAScript の第 6 版で導入されました。、Promise が最初に言語の組み込みメカニズムとして追加されたとき、しかし、それらは以前から、次のようなライブラリの使用を通じてすでに広く使用されていました。 青い鳥

ECMAScript 2020、第 11 版が導入されました Promise.allSettled()、動作が少し異なります。

Promise.allSettled() 静的メソッドは入力としてプロミスの反復可能値を受け取り、単一のプロミスを返します。 約束。 この返された Promise は、入力のすべての Promise が解決されたとき (空の反復可能オブジェクトが渡されたときを含む)、各 Promise の結果を記述するオブジェクトの配列とともに満たされます。

ついに、の登場により、 asyncPromise.all() & Promise.allSettled() に使用できます await いくつかの非同期関数を完了するには:

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

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

問題

Promise.all() は非常に便利な関数ですが、エラー管理に関する動作は実際には非常に注意が必要です。

一方 Promise.allSettled() Promise の結果のステータス、つまり、によって生成された Promise をチェックする必要があります。 Promise.all() 意思 拒否する 入力 Promise のいずれかが失敗した場合。 そしてその場合、それは次のことを意味します await Promise.all(...) 例外がスローされます。

しかし いつ Promise.all() 拒否されても、入力された Promise の一部はまだ実行可能です。
そして、この状況は開発者によって考慮されていないことがよくあります。

次の図とコード スニペットは、この問題を示しています。

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!
})()

問題は、入力 Promise が失敗した場合に、 Promise.all() 意思 早めに拒否する他の Promise を待たずに、非同期に処理することができます。

これは、他の Promise によって実行されるコードが、呼び出し後に実行されるコードと競合する可能性のある操作を実行する、多くの問題のある状況を引き起こす可能性があります。 Promise.all()。 たとえば、データベース接続やファイルの I/O に関する問題が発生することが予想されます。

私の場合、この根本的な問題は、次のような連鎖的な障害の状況で特定されました。 テストスイート: 非同期のコード afterEach() の呼び出しが原因で一部の単体テストが失敗したとき、メソッドは共有リソースがクリーンアップされるのを適切に待機していませんでした。 await Promise.all()

根本的な原因

好奇心から、実装のソースコードを調べてみました。 Promise.all()。 ロジックのほとんどはファイル内にあると思います src/builtins/promise-all.tq V8 JavaScript エンジンで。
.tq ファイルが書き込まれます トルク、C++ にトランスパイルされる V8 固有の高級言語。 したがって、「短絡」の Promise 拒否ロジックが実装のどこで正確に発生するかを理解するのは難しいことがわかりました 😅。

好奇心旺盛な読者のために、優れた資料に Torque 言語に関するセクションがあります。 Daniel Bevenius による learning-v8 リポジトリ

解決策

この問題に関しては、 Promise.allSettled() より良い代替品です Promise.all()、この関数にパラメータとして渡されたすべての Promise の完了を待機します。 実際には、 allSettled EcmaScript が導入されたのは、まさにこの理由のためです。 オリジナルの提案文書 明らかにします:

Promise.allSettled は、すべての入力値を常に待機するという点で独特です。

しかし、 Promise.allSettled() 忘れられがちな潜在的な拒否を自分で処理するか、定型コードを繰り返し作成する必要があります。

私が提案する解決策は、 ドロップイン金庫の代替品 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);
}

これは実際には ジェネリックを使用した TypeScript、ただし、単に削除することもできます。 : types そして 有効な Javascript コードを取得するには:

Javascript の waitForPromises
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);
}

この機能をテストするには、次のように置き換えます。 Promise.all による waitForPromises この記事の最初のコード スニペットにあります。

場合によっては「短絡」動作が発生することもありますが、 Promise.all 便利だと思います waitForPromises ほとんどの状況において、これはより優れた安全な代替手段であり、デフォルトのオプションとして選択する必要があります。 await いくつかの非同期関数の完了。

(おかげで Reddit ユーザー @senocular このブログ投稿に関する非常に関連性の高いフィードバックについては)

さらに読む

#Promise.all #の厄介な落とし穴とその解決策

執筆者について: nipponese

Nipponese News編集部は、国内外のニュースを日本語で分かりやすくお届けします。