`Promise.all()`だと全てのプロミスが完了するのを待つことはできない件

`Promise.all()`だと全てのプロミスが完了するのを待つことはできない件

Clock Icon2024.09.05

こんばんは、リテールアプリ共創部のmorimorikochanです。

Promise.all()を扱ったコードを書いていて、注意しないといけない点を改めて思い出したので、せっかくなのでブログにしたいと思います。

エラーログが一部しか出力されない...?

みなさんがよくJavascriptやTypeScriptで並行処理を行いたい場合、Promise.all()を利用する場面があると思います。
例えばバッチ処理を実行する際に、以下のように各種ファイルをチェックするようなケースです。

const [hoge, fuga, piyo] = await Promise.all([
  validateHoge(),
  validateFuga(),
  validatePiyo(),
])

const validateHoge = () => {
  // この中でチェックに失敗したら`console.error("Hoge処理に失敗しました")`してエラーをthrowしている
}
const validateFuga = () => {
  // この中でチェックに失敗したら`console.error("Fuga処理に失敗しました")`してエラーをthrowしている
}
const validatePiyo = () => {
  // この中でチェックに失敗したら`console.error("Piyo処理に失敗しました")`してエラーをthrowしている
}

実はこのような場合に、複数のプロミスでエラーが発生し拒否された場合、それら全てのエラーログが出力されるとは限りません。

これに気づかずにバッチ処理でPromise.all()を利用してしまうと、エラーが発生した際にエラーログの内容に沿って修正して再実行してもまた違うプロミスで別のエラーログが出力され、また修正を行い、、などというように、エラーログが部分的にしか出力されないため、運用が非効率になってしまう可能性があります。

なぜか

Promise.all()の仕様として、いずれかのプロミスで拒否された場合それ以外のプロミスが完了するまで待つことはなく処理が打ち切られる可能性があるためです。
こう言うような場合は Promise.allSettled()を利用して、全てのプロミスが完了状態に移行するまで処理を待たせましょう

例えば上記の例だと以下のように書き換えることで全てのプロミスのエラーログを出力し切ることができます。

  const [hoge, fuga, piyo] = await Promise.allSettled([
    validateHoge(),
    validateFuga(),
    validatePiyo()
  ]).then((results) => {
    if (results.some((result) => result.status === 'rejected')) {
      throw new Error('main');
    }

    return [
      (results[0] as PromiseFulfilledResult<string>).value,
      (results[1] as PromiseFulfilledResult<string>).value,
      (results[2] as PromiseFulfilledResult<string>).value,
    ];
  });

この仕様はMDNにきちんと記載があります。

Promise.all() は、入力されたプロミスのいずれかが拒否されると直ちに拒否されます。それに対して、Promise.allSettled() が返すプロミスは、入力されたプロミスのいずれかが拒否されたかどうかに関わらず、すべての入力されたプロミスが完了するのを待ちます。入力された反復可能オブジェクトに含まれるプロミスのすべての最終結果が必要な場合は、allSettled() を使用してください。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/all

また、Promise.allSettled()を利用する方法以外にも、そもそも各プロミス内でエラーが発生しても拒否(reject)させずに履行(fullfiled)させることで全ての完了を待つことも可能です。

以下の例ではnullを返却していますが、Result型を導入することでより型安全に扱うことも可能です

// どのプロミスも拒否(`reject`)されていないので全てのプロミスの完了を待つことができる
const [hoge, fuga, piyo] = await Promise.all([
  validateHoge().catch(err=>null),
  validateFuga().catch(err=>null),
  validatePiyo().catch(err=>null),
])

まとめ

  • 全てのプロミスの完了を待ちたい場合は必ずPromise.allSettled()を利用しましょう
  • ある種のレースコンディションを引き起こし、再現性が低い問題になってしまうので注意しましょうす

検証コード

検証コード
export const main = async () => {
  await Promise.all([
    throwErrorOn5Sec(),
    throwErrorOn3Sec(), //このプロミスからのエラーログしか出力されない
    throwErrorOn7Sec(),
  ]);
};

const throwErrorOn3Sec = async () => {
  await wait(3000);
  console.log('ERROR_LOG_ON_3SEC');
  throw new Error('throwErrorOn3Sec');
};

const throwErrorOn5Sec = async () => {
  await wait(5000);
  console.log('ERROR_LOG_ON_5SEC');
  throw new Error('throwErrorOn5Sec');
};

const throwErrorOn7Sec = async () => {
  await wait(7000);
  console.log('ERROR_LOG_ON_7SEC');
  throw new Error('throwErrorOn7Sec');
};

const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

main();

この記事をシェアする

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.