2022年2月20日

Promise API

Promiseクラスには6つの静的メソッドがあります。ここではそれらのユースケースを簡単に説明します。

Promise.all

多くのPromiseを並行して実行し、すべてが準備完了するまで待機したいとします。

例えば、複数のURLを並行してダウンロードし、すべて完了したらコンテンツを処理するなどです。

これがPromise.allの目的です。

構文は以下の通りです。

let promise = Promise.all(iterable);

Promise.allは、イテラブル(通常はPromiseの配列)を受け取り、新しいPromiseを返します。

新しいPromiseは、リストされたすべてのPromiseが解決されると解決され、それらの結果の配列がその結果となります。

例えば、以下のPromise.allは3秒後に確定し、その結果は配列[1, 2, 3]になります。

Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
]).then(alert); // 1,2,3 when promises are ready: each promise contributes an array member

結果の配列メンバーの順序は、元のPromiseと同じであることに注意してください。最初のPromiseの解決に最も時間がかかっても、結果の配列では最初にきます。

一般的な方法は、ジョブデータの配列をPromiseの配列にマッピングし、それをPromise.allでラップすることです。

例えば、URLの配列がある場合、次のようにすべてをフェッチできます。

let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://api.github.com/users/jeresig'
];

// map every url to the promise of the fetch
let requests = urls.map(url => fetch(url));

// Promise.all waits until all jobs are resolved
Promise.all(requests)
  .then(responses => responses.forEach(
    response => alert(`${response.url}: ${response.status}`)
  ));

名前でGitHubユーザーの配列のユーザー情報をフェッチするより大きな例(IDで商品の配列をフェッチすることもできます。ロジックは同じです)

let names = ['iliakan', 'remy', 'jeresig'];

let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));

Promise.all(requests)
  .then(responses => {
    // all responses are resolved successfully
    for(let response of responses) {
      alert(`${response.url}: ${response.status}`); // shows 200 for every url
    }

    return responses;
  })
  // map array of responses into an array of response.json() to read their content
  .then(responses => Promise.all(responses.map(r => r.json())))
  // all JSON answers are parsed: "users" is the array of them
  .then(users => users.forEach(user => alert(user.name)));

いずれかのPromiseが拒否された場合、Promise.allによって返されるPromiseは、そのエラーですぐに拒否されます。

例えば

Promise.all([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(alert); // Error: Whoops!

ここでは、2番目のPromiseが2秒で拒否されます。これにより、Promise.allがすぐに拒否されるため、.catchが実行されます。拒否エラーがPromise.all全体の結果になります。

エラーの場合、他のPromiseは無視されます

1つのPromiseが拒否されると、Promise.allはすぐさま拒否し、リスト内の他のPromiseを完全に無視します。それらの結果は無視されます。

たとえば、上記の例のように複数のfetch呼び出しがあり、1つが失敗した場合、他の呼び出しは引き続き実行されますが、Promise.allはそれらを監視しなくなります。それらは多分確定しますが、結果は無視されます。

Promise.allはそれらをキャンセルするためには何もしません。Promiseには「キャンセル」という概念がないからです。別の章で、それを支援できるAbortControllerについて説明しますが、それはPromise APIの一部ではありません。

Promise.all(iterable) は、iterable にPromiseではない「通常の」値を許可します

通常、Promise.all(...)はPromiseのイテラブル(ほとんどの場合配列)を受け入れます。しかし、それらのオブジェクトのいずれかがPromiseでない場合、結果の配列に「そのまま」渡されます。

例えば、ここで結果は[1, 2, 3]になります。

Promise.all([
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000)
  }),
  2,
  3
]).then(alert); // 1, 2, 3

したがって、都合の良い場所に、準備完了した値をPromise.allに渡すことができます。

Promise.allSettled

最近追加された機能
これは最近言語に追加されたものです。古いブラウザではpolyfillが必要な場合があります。

いずれかのPromiseが拒否されると、Promise.allは全体として拒否します。これは、すべての結果が成功する必要がある「オールオアナッシング」のケースに適しています。

Promise.all([
  fetch('/template.html'),
  fetch('/style.css'),
  fetch('/data.json')
]).then(render); // render method needs results of all fetches

Promise.allSettledは、結果に関係なく、すべてのPromiseが確定するのを待機します。結果の配列には以下が含まれます。

  • 成功したレスポンスの場合は{status:"fulfilled", value:result}
  • エラーの場合は{status:"rejected", reason:error}

例えば、複数のユーザーの情報を取得したいとします。1つのリクエストが失敗した場合でも、他のリクエストには関心があります。

Promise.allSettledを使用してみましょう

let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://no-such-url'
];

Promise.allSettled(urls.map(url => fetch(url)))
  .then(results => { // (*)
    results.forEach((result, num) => {
      if (result.status == "fulfilled") {
        alert(`${urls[num]}: ${result.value.status}`);
      }
      if (result.status == "rejected") {
        alert(`${urls[num]}: ${result.reason}`);
      }
    });
  });

上記の(*)行のresultsは、以下のようになります。

[
  {status: 'fulfilled', value: ...response...},
  {status: 'fulfilled', value: ...response...},
  {status: 'rejected', reason: ...error object...}
]

したがって、Promiseごとにそのステータスとvalue/errorを取得します。

polyfill

ブラウザがPromise.allSettledをサポートしていない場合は、polyfillが簡単です

if (!Promise.allSettled) {
  const rejectHandler = reason => ({ status: 'rejected', reason });

  const resolveHandler = value => ({ status: 'fulfilled', value });

  Promise.allSettled = function (promises) {
    const convertedPromises = promises.map(p => Promise.resolve(p).then(resolveHandler, rejectHandler));
    return Promise.all(convertedPromises);
  };
}

このコードでは、promises.mapは入力値を受け取り、p => Promise.resolve(p)でそれらをPromiseに変換し(念のためPromiseではないものが渡された場合)、すべてに.thenハンドラーを追加します。

そのハンドラーは、成功した結果のvalue{status:'fulfilled', value}に変換し、エラーのreason{status:'rejected', reason}に変換します。これはまさにPromise.allSettledの形式です。

これで、Promise.allSettledを使用して、一部が拒否された場合でも、指定されたすべてのPromiseの結果を取得できます。

Promise.race

Promise.allに似ていますが、最初に確定したPromiseのみを待ち、その結果(またはエラー)を取得します。

構文は以下の通りです。

let promise = Promise.race(iterable);

例えば、ここでの結果は1になります。

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1

ここでの最初のPromiseが最速だったため、それが結果になりました。最初に確定したPromiseが「レースに勝つ」と、それ以降の結果/エラーはすべて無視されます。

Promise.any

Promise.raceに似ていますが、最初に解決されたPromiseのみを待機し、その結果を取得します。指定されたすべてのPromiseが拒否された場合、返されるPromiseはAggregateErrorで拒否されます。これは、すべてのPromiseエラーをerrorsプロパティに保存する特別なエラーオブジェクトです。

構文は以下の通りです。

let promise = Promise.any(iterable);

例えば、ここでの結果は1になります。

Promise.any([
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 1000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1

ここでの最初のPromiseが最速でしたが拒否されたため、2番目のPromiseが結果になりました。最初に解決されたPromiseが「レースに勝つ」と、それ以降の結果はすべて無視されます。

すべてのPromiseが失敗した場合の例を次に示します。

Promise.any([
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ouch!")), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Error!")), 2000))
]).catch(error => {
  console.log(error.constructor.name); // AggregateError
  console.log(error.errors[0]); // Error: Ouch!
  console.log(error.errors[1]); // Error: Error!
});

ご覧のとおり、失敗したPromiseのエラーオブジェクトは、AggregateErrorオブジェクトのerrorsプロパティで利用できます。

Promise.resolve/reject

Promise.resolveおよびPromise.rejectメソッドは、async/await構文(後ほど説明します)によってある程度不要になるため、現代のコードではめったに必要ありません。

ここでは、完全を期すため、および何らかの理由でasync/awaitを使用できない人のために説明します。

Promise.resolve

Promise.resolve(value)は、結果valueを使用して解決済みのPromiseを作成します。

以下と同じです

let promise = new Promise(resolve => resolve(value));

このメソッドは、関数がPromiseを返すことが期待される場合の互換性のために使用されます。

例えば、次のloadCached関数はURLをフェッチし、その内容を記憶(キャッシュ)します。同じURLでの将来の呼び出しの場合、キャッシュから以前の内容をすぐに取得しますが、Promise.resolveを使用してそのPromiseを作成し、返される値が常にPromiseになるようにします。

let cache = new Map();

function loadCached(url) {
  if (cache.has(url)) {
    return Promise.resolve(cache.get(url)); // (*)
  }

  return fetch(url)
    .then(response => response.text())
    .then(text => {
      cache.set(url,text);
      return text;
    });
}

関数がPromiseを返すことが保証されているため、loadCached(url).then(…)と記述できます。loadCachedの後に常に.thenを使用できます。これが(*)行のPromise.resolveの目的です。

Promise.reject

Promise.reject(error)は、errorで拒否されたPromiseを作成します。

以下と同じです

let promise = new Promise((resolve, reject) => reject(error));

実際には、このメソッドはほとんど使用されません。

まとめ

Promiseクラスには6つの静的メソッドがあります。

  1. Promise.all(promises) – すべてのPromiseが解決されるのを待ち、それらの結果の配列を返します。指定されたPromiseのいずれかが拒否された場合、それはPromise.allのエラーになり、他のすべての結果は無視されます。
  2. Promise.allSettled(promises)(最近追加されたメソッド)– すべてのPromiseが確定するのを待ち、それらの結果を次のオブジェクトの配列として返します。
    • status: "fulfilled" または "rejected"
    • value (解決した場合) または reason (拒否した場合)。
  3. Promise.race(promises) – 最初に確定したPromiseを待機し、その結果/エラーが結果になります。
  4. Promise.any(promises)(最近追加されたメソッド)– 最初に解決されるPromiseを待機し、その結果が結果になります。指定されたすべてのPromiseが拒否された場合、AggregateErrorPromise.anyのエラーになります。
  5. Promise.resolve(value) – 指定された値で解決済みのPromiseを作成します。
  6. Promise.reject(error) – 指定されたエラーで拒否されたPromiseを作成します。

これらの中で、Promise.allが実際には最も一般的です。

チュートリアルマップ

コメント

コメントする前に読んでください…
  • 改善点に関する提案がある場合は、コメントする代わりに、GitHub issueを送信するか、プルリクエストを送信してください。
  • 記事の内容を理解できない場合は、詳しく説明してください。
  • コードを数単語挿入するには、<code>タグを使用し、複数行の場合は<pre>タグで囲み、10行を超える場合はサンドボックス(plnkrjsbincodepen...)を使用してください。