JavaScriptでPromise.all()ではなくPromise.allSettled()を使うべき

Promise.allSettled()を使用するべき理由

JavaScriptで複数の非同期処理を並列実行する際、多くの人が Promise.all() を使用します。

しかし、実務では Promise.all() よりも Promise.allSettled() を使うほうが安全なケースが多いです。

Promise.all()の問題点

Promise.all() はすべてのPromiseが成功した場合のみ結果を返します。

例えば、以下のコードはすべて成功するので結果を返します。

JavaScript
const task = async (id, ms) => {
  const response = await fetch(
    `https://dummyjson.com/products/${id}?select=id,title&delay=${ms}`
  )

  if (!response.ok) {
    throw new Error(`id:${id} HTTP ${response.status}`)
  }

  const data = await response.json()

  return `id:${id} ${data.title} ${ms}ms 完了`
}

async function main() {
  try {
    const results = await Promise.all([
      task(1, Math.floor(Math.random() * 3000)),
      task(2, Math.floor(Math.random() * 3000)),
      task(3, Math.floor(Math.random() * 3000))
    ])
    console.log('すべて完了')
    console.log(results)
    // [
    //   "id:1 Essence Mascara Lash Princess 1179ms 完了",
    //   "id:2 Eyeshadow Palette with Mirror 1543ms 完了",
    //   "id:3 Powder Canister 2806ms 完了"
    // ]
  } catch (error) {
    console.error('エラー発生:', error.message)
  }
}

main()

しかし、次のコードは idの2番目が0になっており、idが0のリクエストは失敗するため、すべての結果を返しません。

JavaScript
const task = async (id, ms) => {
  const response = await fetch(
    `https://dummyjson.com/products/${id}?select=id,title&delay=${ms}`
  )

  if (!response.ok) {
    throw new Error(`id:${id} HTTP ${response.status}`)
  }

  const data = await response.json()

  return `id:${id} ${data.title} ${ms}ms 完了`
}

async function main() {
  try {
    const results = await Promise.all([
      task(1, Math.floor(Math.random() * 3000)),
      task(0, Math.floor(Math.random() * 3000)),
      task(3, Math.floor(Math.random() * 3000))
    ])
    console.log('すべて完了')
    console.log(results)
  } catch (error) {
    console.error('エラー発生:', error.message)
    // エラー発生: id:0 HTTP 404
  }
}

main()

これだと他の2つが成功していたとしても結果が表示されず、1つが失敗すると処理が終了してしまいます。

また、他の結果は成功と失敗のどちらなのかがわからないという欠点もあります。

例えば、/api/foo、/api/bar、/api/baz のようなAPIをPromise.allで取得して、画面上にそれぞれのデータを表示させる処理をした場合は、1つが失敗すると全部表示されません。

Promise.allSettled()ならすべての結果がわかる

前述のPromise.all()をPromise.allSettled()に変えると、すべてのPromiseが成功・失敗に関係なく完了するまで待ち、結果を配列で取得できます。

JavaScript
const task = async (id, ms) => {
  const response = await fetch(
    `https://dummyjson.com/products/${id}?select=id,title&delay=${ms}`
  )

  if (!response.ok) {
    throw new Error(`id:${id} HTTP ${response.status}`)
  }

  const data = await response.json()

  return `id:${id} ${data.title} ${ms}ms 完了`
}

async function main() {
  const results = await Promise.allSettled([
    task(1, Math.floor(Math.random() * 3000)),
    task(0, Math.floor(Math.random() * 3000)),
    task(3, Math.floor(Math.random() * 3000))
  ])
  console.log('すべて完了')

  results.forEach(result => {
    if (result.status === 'fulfilled') {
      console.log(result.value)
      // id:1 Essence Mascara Lash Princess 2600ms 完了
      // id:3 Powder Canister 2684ms 完了
      return
    }

    console.error(result.reason.message)
    // id:0 HTTP 404
  })
}

main()

すべてのデータがないと処理不能なケースでなければ、このように処理されるほうがデバッグやテストなどもしやすいため都合が良いです。

Promise.allSettled() は失敗 (status: 'rejected') のときはvalueではなくreasonで値を返すので、この点は間違いやすいので注意が必要です。

JavaScript
Promise.allSettled([
  Promise.resolve(123),
  new Promise((resolve) => setTimeout(() => resolve(456), 2000)),
  Promise.reject(new Error('error')),
]).then((values) => console.log(values));
// [
//   { status: 'fulfilled', value: 123 },
//   { status: 'fulfilled', value: 456 },
//   { status: 'rejected', reason: Error: error }
// ]

Promise.allSettled() は Promise.all() より新しい機能

Promise.all() は2015年からあるメソッドですが、Promise.allSettled() が登場したのは2020年です。

そのため、古いWebサイトでは使用されていなかったり、メソッドの存在自体を把握していないケースも珍しくありません。

メソッド ECMAScript
Promise.all() ES2015 (ES6)
Promise.allSettled() ES2020

ちなみに Promise.allSettled() は、現在すべてのブラウザが対応しています。

まとめ

複数の非同期処理を並列実行する場合、すべて成功するとは限りません。

そのため、結果をすべて確認したい場合は Promise.allSettled() を使用するほうが安全です。