Promise は非同期処理の最終的な完了もしくは失敗を表すオブジェクトです。多くの人々は既存の用意された Promise を使うことになるため、このガイドでは、Promise の作成方法の前に、関数が返す Promise の使い方から説明します。

本質的に、Promise はコールバックを関数に渡すかわりに、関数が返したオブジェクトに対してコールバックを登録するようにする、というものです。

例えば createAudioFileAsync() という非同期に音声ファイルを生成する関数を考えましょう。この関数はコンフィグオブジェクトと 2 つのコールバック関数を受け取り、片方のコールバックは音声ファイルが無事作成されたときに呼び出され、もう一つはエラーが発生したときに呼び出されます。

以下のコードは createAudioFileAsync() を使用したものです。

function successCallback(result) {
  console.log("Audio file ready at URL: " + result);
}

function failureCallback(error) {
  console.log("Error generating audio file: " + error);
}

createAudioFileAsync(audioSettings, successCallback, failureCallback);

最近では関数は Promise を返し、代わりにその Promise にコールバックを登録することができます。

もし createAudioFileAsync() が Promise を返すように書き換えられたとすれば、以下のようにシンプルに使用することができます。

createAudioFileAsync(audioSettings).then(successCallback, failureCallback);

これは以下のコードの短縮形です。

const promise = createAudioFileAsync(audioSettings); 
promise.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);

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

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

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

基本的に、それぞれの 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);

重要: コールバック関数から処理結果を返すのを忘れないでください。さもないと後続のコールバック関数からその処理結果を利用することができなくなります (アロー関数を使った () => x は () => { return x; } の短縮形です)。

catch後のチェーン

失敗、つまり catch の後にチェーンするのも可能で、これはチェーン内の動作が失敗した後でも新しい動作を行うのに便利です。次の例を読んでください:

new Promise((resolve, reject) => {
    console.log('Initial');

    resolve();
})
.then(() => {
    throw new Error('Something failed');
        
    console.log('Do this');
})
.catch(() => {
    console.log('Do that');
})
.then(() => {
    console.log('Do this whatever happened before');
});

これは下記のテキストを出力します:

Initial
Do that
Do this whatever happened before

注意:Do this のテキストは Something failed エラーが reject を引き起こしたため出力されないことに注意してください。

エラーの伝搬

以前の悲運のピラミッド形式の記述方法では failureCallback を 3 回書く必要がありましたが、Promise チェーンでは failureCallback は 1 回で済みます。

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

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

try {
  const result = syncDoSomething();
  const newResult = syncDoSomethingElse(result);
  const 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);
  }
}

async/await は Promise の上に成り立っています。例えば上記の doSomething() はこれまでと同じ(Promise を返す)関数です。この書き方の詳細についてはこちらをご覧ください。

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

Promise の失敗イベント

Promise が失敗するたびに、グローバルスコープ(通常 window オブジェクトか、Web Worker 内ならば Worker か Worker のインターフェイスをもつオブジェクト)に以下の 2 つのイベントのどちらかが送られます。

rejectionhandled
Promise が失敗したあと、それが catch ハンドラーなどによってハンドルされたあとに送られる。
unhandledrejection
Promise が失敗したあと、ハンドラーが存在しないときに送られる。

いずれの場合でも、イベントオブジェクト(PromiseRejectionEvent 型)は失敗した Promise を表す promise プロパティと、その Promise が失敗した理由を表す reason プロパティを持ちます。

これらのイベントを使えば、Promise のエラーハンドラーのフォールバックを指定することができ、また Promise を管理する際の問題をデバッグするのにも役立ちます。これらのイベントのハンドラーはグローバルであり、どこから発生したかに関わらず、すべてのエラーは同じイベントハンドラーによって処理されます。

特に便利なケースとして、Node.js 用のコードを書いているときにプロジェクト内のモジュールで Promise が失敗しハンドルされないことはよくあります。これらは Node.js の実行環境によりコンソールに出力されます。これらの失敗を分析したいときやハンドラーを設定したいとき、あるいは単にコンソールがこれらで埋め尽くされないようにしたいとき、以下のように unhandledrejection イベントのハンドラーを追加することができます。

window.addEventListener("unhandledrejection", event => {
  /* ここで該当の Promise を event.promise で、失敗の理由を
     event.reason で取得して調べることができます */

  event.preventDefault();
}, false);

イベントの preventDefault() メソッドを呼び出すことによって、失敗した Promise がハンドルされないときの JavaScript の実行環境のデフォルトの動作を防ぐことができます。Node.js を含む通常の実行環境ではデフォルトの動作ではエラーがコンソールに出力されます。

当然理想的には、これらのイベントを捨てる前に失敗した Promise を調べて、いずれもコードのバグによるものではないことを確かめるべきです。

古いコールバック API をラップする Promise の作成

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

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

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

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

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

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

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

基本的に、Promise のコンストラクタには、手動で Promise を resolve もしくは reject できるようにする実行関数を渡します。setTimeout は失敗することはないので、reject は省略しました。

合成 (Composition)

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

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

以下のように複数の処理を並行に開始し、すべてが終了するのを待つことができます。

Promise.all([func1(), func2(), func3()])
.then(([result1, result2, result3]) => { /* result1, result2, result3 が使える */ });

以下のように工夫すれば、逐次実行をする直列的な合成も記述することができます。

[func1, func2].reduce((p, f) => p.then(f), Promise.resolve())
.then(result3 => { /* result3 が使える */ });

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

このような処理は以下のように、関数型プログラミングでよくある再利用可能な合成関数にすることがすることができます。

const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));

composeAsync 関数は任意の個数の関数を引数として受け取って、1本のパイプラインとして合成された関数を返します。この関数に渡された初期値は合成された関数を通過していきます。

const transformData = composeAsync(func1, func2, func3);
const result3 = transformData(data);

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

let result;
for (const f of [func1, func2, func3]) {
  result = await f(result);
}
/* 最終的な結果(result3)が使える */

タイミング

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

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

渡された関数は、すぐに実行されるのではなくマイクロタスクのキューに入れられます。現在のイベントループの終わりにこのキューは空にされ、この関数が実行されます(つまりかなり早い段階です)。

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

ネスト

単純な Promise チェーンならば、ネストは不用意な合成の結果生まれるものなので、ネストせずに平らにしておくのがベストです。よくある間違いを参照してください。

ネストとは catch ステートメントのスコープを制限するための制御構造です。正確には、ネストされた catch はそのスコープ内の失敗しかキャッチせず、Promise チェーン上でスコープ外のエラーには反応しません。正しく使えばより正確にエラーからの回復ができるようになります。

doSomethingCritical()
.then(result => doSomethingOptional(result)
  .then(optionalResult => doSomethingExtraNice(optionalResult))
  .catch(e => {})) // オプションの処理が失敗すれば無視して進める
.then(() => moreCriticalStuff())
.catch(e => console.log("Critical failure: " + e.message));

インデントではなく括弧 () によってオプションの処理がネストされていることに注意してください。

内側の catch ステートメントは doSomethingOptional() と doSomethingExtraNice() からの失敗だけをキャッチし、キャッチしたあと moreCriticalStuff() へと処理が続きます。重要なのは、もし doSomethingCritical() が失敗したらそのエラーは最後の catch によってだけキャッチされるということです。

よくある間違い

Promise チェーンを合成するときは以下のようなよくある間違いに気をつける必要があります。以下の例にいくつかの間違いが含まれています。

// 悪い例。間違いを 3 つ見つけてください。

doSomething().then(function(result) {
  doSomethingElse(result) // 内側のチェーンから Promise を返していない + 不必要なネスト
  .then(newResult => doThirdThing(newResult));
}).then(() => doFourthThing());
// チェーンの最後を catch で終わらせていない

最初の間違いは適切にチェーンを構成できていないことです。これは、新しい Promise を作成したがそれを返すのを忘れているときに起きます。結果としてチェーンは壊れ、2 つのチェーンが独立に実行されることになります。これはつまり doFourthThing() は doSomethingElse() や doThirdThing() の終了を待たないことになり、おそらく意図せず並行して実行されることになります。別々のチェーンでは別々のエラーハンドリングが行われるため、キャッチされないエラーが発生することになります。

2 つ目の間違いは不必要にネストしていることであり、1 つめの間違いを可能にしているものでもあります。ネストするということは内側のエラーハンドラーが制限されるということであり、もしこれが意図していないものであれば、エラーがキャッチされないということが起こりえます。これの変化形で Promise コンストラクターアンチパターンというものがあり、ネストに加えて、Promise を既に使用しているコードを不必要な Promise コンストラクターでラップするというものです。

3 つ目の間違いはチェーンを catch で終わらせていないことです。ほとんどのブラウザーではそのようなチェーンは Promise の失敗がキャッチされないことになります。

よい経験則としては、Promise チェーンは常に return するか catch で終わらせ、新しい Promise を得るたびにすぐに return してチェーンを平らにすることです。

doSomething()
.then(function(result) {
  return doSomethingElse(result);
})
.then(newResult => doThirdThing(newResult))
.then(() => doFourthThing())
.catch(error => console.log(error));

() => x は () => { return x; } の短縮形であることに注意してください。

これで適切なエラーハンドリングがされた 1本のチェーンができました。

async/await を使えば、すべてではないにしてもほとんどの問題は解決しますが、このシンタックスで最もよくある間違いが await キーワードを忘れることであるという欠点があります。

関連項目

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

最終更新者: ecormaksin,