MDN’s new design is in Beta! A sneak peek: https://blog.mozilla.org/opendesign/mdns-new-design-beta/

Using promises

 

Promiseは非同期処理の最終的な完了もしくは失敗を表すオブジェクトです。Promiseはコンストラクタを使って生成されるでしょう。しかし、多くの人々は関数によって返される既に生成されたPromiseを使うことになります。したがって、このガイドでは、関数が返すPromiseの使い方から説明します。

要するに、Promiseはコールバックを関数に渡すかわりにコールバックを付属させるリターンされたオブジェクトです。

例えば、昔ながらの形式で、2つのコールバックを受け取り、最終的な成否に応じて一方を呼び出す関数を書くと次のようになります。

function successCallback(result) {
  console.log("It succeeded with " + result);
}

function failureCallback(error) {
  console.log("It failed with " + error);
}

doSomething(successCallback, failureCallback);

Promiseを返す近代的な関数を使うと、2つのコールバックを使って次のように記述できます。

let promise = doSomething();
promise.then(successCallback, failureCallback);

もしくは、単純に以下のように記述しても構いません。

doSomething().then(successCallback, failureCallback);

これを非同期関数呼び出し(asynchronnous function call)と呼びます。この記述方法の利点を順に説明します。

保証

旧来のコールバック渡しとは異なり、Promiseでは以下が保証されています。

  • コールバックは決してJavaScriptイベントループの実行完了より前には呼び出されない。
  • 上記の通り、.thenで付加されたコールバックは非同期処理の完了もしくは失敗のさらにに呼び出される。
  • .thenを何回も呼び出して複数のコールバックを追加してもよく、それぞれのコールバックは追加順に独立して実行される。

それよりも、最もすぐわかるPromiseの利点はPromiseチェーンです。

Promiseチェーン

一般的なニーズとしては、複数の非同期処理を順番に実行し、前の処理の結果を次の処理で使うというものがあります。これはPromiseチェーンを作成することで行えます。

さあ魔法の時間です。 then 関数は元のPromiseとは別の新しいPromiseを返します。

const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);

もしくは、以下のように書いても構いません。

let promise2 = doSomething().then(successCallback, failureCallback);

2つ目のPromiseはdoSomething()の完了を表すだけではなく、渡した successCallback もしくは failureCallback の完了も表し、これらのコールバックはPromiseを返す別の非同期関数であっても構いません。この場合、 promise2 に追加されたコールバックはどれも successCallback または failureCallbackの後ろに追加されることになります。

基本的に、それぞれのPromiseはチェーン(連鎖)させた別の非同期処理の完了を表します。

これまで、複数の非同期処理を順番に実行するには、昔ながらのコールバックのピラミッドを作る必要がありました。

doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Got the final result: ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

近代的な関数を使えば、戻り値のPromiseにコールバックを付加してPromiseチェーンとして記述できます。

doSomething().then(function(result) {
  return doSomethingElse(result);
})
.then(function(newResult) {
  return doThirdThing(newResult);
})
.then(function(finalResult) {
  console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);

 then 関数の引数はオプションです。また、 catch(failureCallback)then(null, failureCallback)を短く記述するものです。記述には アロー関数 を使っても構いません。

doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
  console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);

重要: 常にPromiseを返すようにしてください。さもないとチェーンされなくなり、catchでエラーをとらえることができなくなります。

エラーの伝搬

Promiseチェーンでは failureCallback は多くとも1度しか実行されない一方で、以前のピラミッド形式の記述方法では failureCallback が3度実行される可能性があります。

doSomething()
.then(result => doSomethingElse(value))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback);

基本的に、Promiseチェーンでは例外が発生するとチェーン(連鎖)が止まり、チェーンをたどって catch ハンドラを探します。この振る舞いは同期的なコードの動作と一致します。

try {
  let result = syncDoSomething();
  let newResult = syncDoSomethingElse(result);
  let finalResult = syncDoThirdThing(newResult);
  console.log(`Got the final result: ${finalResult}`);
} catch(error) {
  failureCallback(error);
}

同期的なコードとの一致はECMAScript 2017のシンタックスシュガー async/await を使うことでより完全なものとなります。

async function foo() {
  try {
    let result = await doSomething();
    let newResult = await doSomethingElse(result);
    let finalResult = await doThirdThing(newResult);
    console.log(`Got the final result: ${finalResult}`);
  } catch(error) {
    failureCallback(error);
  }
}

これはPromiseにより動作し、これまでの例と同一の関数 doSomething() を使うことができます。この書き方の詳細についてはこちらをご覧ください。

Promiseは例外やプログラミングエラーを含むすべてのエラーをとらえることで、ピラミッド型のコールバックの根本的な問題を解決します。これは非同期処理の関数合成(Composition)に不可欠のものです。

古いコールバックAPIに関連したPromiseの作成

Promise はコンストラクタを使って1から作ることもできます。これは古いAPIをラップする場合にのみ必要となるはずです。

理想的には、すべての非同期関数はPromiseを返しているはずでしたが、残念ながらAPIの中にはいまだに成功/失敗用のコールバックを渡すという古いやり方をしているものがあります。典型的な例としてはsetTimeout()関数があります。

setTimeout(() => saySomething("10 seconds passed"), 10000);

古い形式のコールバックとPromiseの混在は問題を引き起こします。というのは、 saySomething が失敗したりプログラミングエラーを含んでいた場合にそのエラーをとらえられないからです。

幸いにもPromiseの中にラップすることができます。ベストプラクティスは、問題のある関数を最も可能性の低いレベルでラップした上で、決して直接呼ばないというものです。

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

wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);

基本的に、Promiseのコンストラクタは、手動でPromiseをresolveもしくはrejectできるようにするexecutor関数を取ります。 setTimeout の場合、失敗することはないので、rejectすることになります。

合成(Composition)

Promise.resolve()Promise.reject() はそれぞれresolveもしくはrejectされたPromiseを手動で作成するショートカットで、たまに役立つことがあります。

Promise.all()Promise.race() は同時並行で実行中の非同期処理の合成(Composition)ツールです。

工夫すれば、直列的な合成も記述することができます。

[func1, func2].reduce((p, f) => p.then(f), Promise.resolve());

基本的に、非同期関数の配列を以下と同等のPromiseチェーンにreduceしています。 Promise.resolve().then(func1).then(func2);

ECMAScript 2017では直列的な合成は async/awaitでもっと簡単に書くことができます。

for (let f of [func1, func2]) {
  await f();
}

タイミング

想定外の事態とならないよう、たとえすでにresolveされたPromiseであっても、 then に渡される関数が同期的に呼ばれることはありません。

Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2

渡された関数は、すぐに実行するのではなくマイクロタスクのキューに入れられます。そして、ほどなくして、JavaScriptイベントループの現在の実行(run)の最後にキューが空になってから実行されます。

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

wait().then(() => console.log(4));
Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
console.log(1); // 1, 2, 3, 4

関連項目

ドキュメントのタグと貢献者

 このページの貢献者: sohopro, tisanyan
 最終更新者: sohopro,