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 つのコールバックを使って次のように記述できます。
const 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);
もしくは、以下のように書いても構いません。
const promise2 = doSomething().then(successCallback, failureCallback);
2 つ目の Promise は doSomething() の完了を表すだけではなく、渡した successCallback もしくは failureCallback の完了も表し、これらのコールバックは Promise を返す別の非同期関数であっても構いません。この場合、 promise2 に追加されたコールバックはどれも 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);
重要: 常に何らかの結果を返すようにしてください。さもないとチェーンされなくなり、catch でエラーをとらえることができなくなります({} が省かれる時、アロー関数は暗示的に return しています)。
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 {
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);
}
}
async/await は Promise の上に成り立っています。例えば上記の doSomething() はこれまでと同じ(Promise を返す)関数です。この書き方の詳細についてはこちらをご覧ください。
Promise は例外やプログラミングエラーを含むすべてのエラーをとらえることで、悲運のピラミッド型コールバックの根本的な問題を解決します。これは非同期処理の関数合成(Composition)に不可欠のものです。
古いコールバック 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() は同時並行で実行中の非同期処理の合成(Composition)ツールです。
工夫すれば、直列的な合成も記述することができます。
[func1, func2].reduce((p, f) => p.then(f), Promise.resolve());
基本的に、非同期関数の配列を以下と同等の Promise チェーンに削減しています。 Promise.resolve().then(func1).then(func2);
これは関数型プログラミングでよくある、再利用可能な合成関数でも可能です:
const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));
composeAsync 関数は任意の個数の関数を引数として受け取って、1本のパイプラインとして合成された関数を返します。この関数に渡された初期値は合成された関数を通過していきます。composeAsync に渡す関数は非同期でも同期的でもよく、正しい順番で実行されることが保証されているので便利です。
let transformData = composeAsync(func1, asyncFunc1, asyncFunc2, func2);
transformData(data);
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
渡された関数は、すぐに実行されるのではなくマイクロタスクのキューに入れられます。現在のイベントループの終わりにこのキューは空にされます(つまりかなり早い段階です)。
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

