📚

Promise.all と Promise.allSettled の違いを理解して適切に使い分けましょう

2022/08/29に公開

🌼 はじめに

実はpromise.allSettledは使ったことはありません!が、今後使いたい場面がありそうなのではっきり整理しておきたいと思います。

1. Promise.all

Promise.allは引数として受け取ったPromiseがすべて成功(fulfilled)したら解決されるプロミスに結果値の配列を返し、一つでも失敗したら残ったPromiseは実行せずすぐエラー処理に移ります。

MDNからサンプルコードをもらってきました。

まずすべてのPromiseが成功する場合です。成功した結果値の配列を返してますね。このとき配列の順番は実行完了順ではなく引数で渡された順と同じになります。

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values); // [3, 42, 'foo']
});

もしひとつでも失敗するPromiseがある場合は最初に失敗した理由をもってエラー処理を走らせます。以下の例で2つの処理が失敗しますが、「すぐ失敗」だけ表示される理由はp4の前にp3が先に失敗したからです。

const p1 = Promise.resolve("成功!");  
const p2 = Promise.resolve("成功!");  
const p3 = Promise.reject("すぐ失敗");
const p4 = setTimeout(() => Promise.reject("後で失敗"), 3000)

Promise.all([p1, p2, p3, p4])
.then(values => {
  console.log(values);
})
.catch(error => {
  console.error(error)
});

//From console:
//"すぐ失敗"

こういう仕様のおかげで、Promiseのいずれかが失敗したときは処理が早く終わるというメリットがあります。逆に失敗したPromiseより完了が遅いPromiseたちは成功するか失敗するかがわからないというデメリットもあります。

2. Promise.allSettled

Promise.allSettled は引数として受け取ったすべてのPromiseが成功(fulfilled)もしくは失敗(rejected)された後に、それぞれのPromiseの結果を記述したオブジェクトの配列を解決されるプロミスに返します

同じくMDNのサンプルコード見てみましょう。

失敗するPromiseがあるにも関わらず、引数で渡されたPromise処理結果を全部オブジェクトの配列で返しています。

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo'));

Promise.allSettled([promise1, promise2]).then((results) => {
  console.log(results) // [{status: 'fulfilled', value: 3}, {status: 'rejected', reason: 'foo'}]
}); 

複数のPromiseが失敗しても全Promiseのstatusを返すので正確に何が失敗するのか全部わかるという点がPromise.allと違うところですね。逆に失敗するPromiseがあっても処理を中断しないので、Promise.allより早くなることは難しいとも言えます。

また、promise.allSettledcatchは不要です。そもそも渡されたすべてのPromiseが拒否されてもcatch文に移りません。

const promise1 = Promise.reject('失敗しました');
const promise2 = Promise.reject('また失敗しました');

Promise.allSettled([promise1, promise2])
  .then((res) => console.log(res))
  .catch(() => console.log('エラーです'));
  
// From console:
// [{status: 'rejected', reason: '失敗しました'}, {status: 'rejected', reason: 'また失敗しました'}]

もし例外処理が必要な場合、もしくは成功値と失敗値を分けてなにかやりたいときは多少手を加える必要があります。

const promise1 = Promise.resolve("成功しました");
const promise2 = Promise.resolve(20000);
const promise3 = Promise.reject("通信が切れました");

Promise.allSettled([promise1, promise2, promise3])
  .then((result) => {
    const fulfilled = result.filter(data => data.status === 'fulfilled').map(data => data.value);
    const rejected = result.filter(data => data.status === 'rejected').map(data => data.reason);
    
    console.log(fulfilled); // ['成功しました', 20000]
    console.log(rejected); // ['通信が切れました']
});

3. いつ何を使うか

どっちも「複数の非同期処理を扱う」という共通点はあっても違う部分もありましたね。これを踏まえていつ何を使ったほうが良いかを整理してみましょう。

3-1. Promise.allのほうがいい

  • Promiseたちがお互いに依存してる場合
  • 一つのPromiseでも失敗したらさっさとエラー処理に移って欲しい場合

例えば、画面を描画するための情報をいくつかのAPIコールで叩いてて、その中で一つでも足りなかったら描画できない場合などが該当すると思います。

3-2. Promise.allSettledのほうがいい

  • Promiseたちがお互い独立的で、一部のPromiseが失敗してもすべてのPromiseを実行してほしい場合
  • 各プロミスの結果を常に知りたい場合

何らかのデータ送信を例にしてみましょう。複数のデータを送信する時一部送信が失敗しても残りデータを止めずに送信してほしいことがあると思います。また送信が失敗しても、どのファイルが送信できなかったか正確に明示してほしいこともあるでしょう。こういう場合はPromise.allSettledが便利だと思います。

4. 注意点

Promise.all(ES2015)よりPromise.allSettled(ES2020)のほうが後で追加された機能なので、ブラウザやnode.jsのバージョンによってはサポートしていない可能性があります。

このサイトでES2020のブラウザサポート状況が見れますので、ご参考お願いします。

http://kangax.github.io/compat-table/es2016plus/

ちなみにnode.jsは v12.9.0 からPromise.allSettledを追加したようですね。

https://nodejs.org/en/blog/release/v12.9.0/

🌷 終わり

これでいつPromise.allSettledを採用するべきか理解できました。やった!

GitHubで編集を提案

Discussion