Promise は、JavaScript や Node.js において、非同期処理のコールバック関数をエレガントに記述するための仕組みです。英語の promise は、「制約」、「保障」などの意味を持ちます。Promise は、Chrome 63, Firefox 58, Safari 11.1, Edge 18, Node.js 4.* から利用可能です。IE11 ではサポートされていません。
JavaScript や Node.js では、ブロックする(処理が終わるまで待ち合わせる)関数よりも、非同期関数(処理の完了を待たず、処理が完了した時点でコールバック関数が呼び出される)の方が多様されます。ここで、例えば、膨大な演算(実は単に元の数を2倍するだけ)を行う非同期関数 aFunc1() があるとします。下記は、100の2倍を求める非同期関数の使用例です。
// 引数を2倍にする非同期関数
function aFunc1(data, callback) {
setTimeout(function() {
callback(data * 2);
}, Math.random() * 1000);
}
function sample_callback() { // 非同期関数を用いて100の2倍を求める aFunc1(100, function(value) { console.log(value); // => 200 }); }
単純に非同期関数を1回だけ呼び出すのであれば、上記で問題ありませんが、1回目で得られた値を用いて、aFunc1() を2度、3度呼び出そうとすると、下記の様な実装になります。
function sample_callback_hell() { aFunc1(100, function(data) { console.log(data); // => 200 aFunc1(data, function(data) { console.log(data); // => 400 aFunc1(data, function(data) { console.log(data); // => 800 }); }); }); }
呼び出す回数に比例してコールバックのネストが深くなります。これを、「コールバック地獄」と呼びます。
非同期関数はまた、処理の順序を制御できないという問題も含みます。下記の例では、100の2倍、200の2倍、400の2倍を求めようとしたにも関わらず、処理結果は 200, 400, 800 だったり、800, 200, 400 など、結果処理が順不同となるという問題があります。
function sample_timing_problem() { aFunc1(100, function(data) { console.log(data); // => 200 }); aFunc1(200, function(data) { console.log(data); // => 400 }); aFunc1(400, function(data) { console.log(data); // => 800 }); }
これらの問題を解決するために考案されたのが Promise です。Promise は、約束、誓約、保証などの意味を持ちます。Promise は、待機(pending)、成功(fulfilled)、失敗(rejected)の3値を持つオブジェクトです。前述の非同期関数 aFunc1() を Promise を用いて書き直すと下記の様になります。処理を行う関数を引数とした Promise オブジェクトを返却するように修正します。
function aFunc2(data) { return new Promise(function(callback) { setTimeout(function() { callback(data * 2); }, Math.random() * 1000); }); }
Promise オブジェクトは then(ok_callback, ng_callback) というメソッドを持ちます。then() は、Promise が成功または失敗になるまで処理を受け流し、成功時に ok_callback を、失敗時に ng_callback をコールバック関数として呼び出します。
function sample_promise() {
aFunc2(100).then(function(data) {
console.log(data); // => 200
});
}
アロー関数を用いると、次のようにも記述できます。
function sample_promise2() {
aFunc2(100).then((data) => {
console.log(data); // => 200
});
}
さらに処理を継続するには、下記の様にします。
function sample_promise3() { aFunc2(100).then((data) => { console.log(data); // => 200 return aFunc2(data); }) .then((data) => { console.log(data); // => 400 return aFunc2(data); }) .then((data) => { console.log(data); // => 800 }); }
Promise のエラー処理について考察します。下記は、約 30% の確率でエラーとなる Promise 非同期関数です。
function aFunc3(data) { return new Promise(function(okCallback, ngCallback) { setTimeout(function() { if (Math.random() < 0.30) { ngCallback(new Error('ERROR!')); } else { okCallback(data * 2); } }, Math.random() * 1000); }); }
.then() は第一引数に成功時のコールバック関数、第二引数に失敗時のコールバック関数を指定します。エラーを考慮した呼び出し元は下記の様になります。
function sample_reject() { aFunc3(100).then( (data) => { console.log(data); }, // 成功時の処理 (e) => { console.log(e); } // 失敗時の処理 ); }
上記は、下記の様に記述することもできます。.catch(ng_callback) は、.then(undefined, ng_callback) と同じ意味を持ちます。Promise は一度エラーが発生すると、最初に ng_callback 関数が指定されるまで、then 処理をスキップします。
function sample_catch() { aFunc3(100).then((data) => { console.log(data); return aFunc3(data); }) .then((data) => { console.log(data); return aFunc3(data); }) .then((data) => { console.log(data); }) .catch((e) => { console.log(e); }); }
.catch() はまた、処理中に発生した throw をキャッチすることもできます。下記の例では、aFunc3() 内部で発生したエラーや、2番目の処理で発生した例外を .catch() が受け止めます。
function sample_catch_with_throw() { aFunc3(100).then((data) => { console.log(data); return aFunc3(data); }) .then((data) => { console.log(data); throw new Error('ERROR!!!'); }) .then((data) => { console.log(data); }) .catch((e) => { console.log(e); }); }
.catch() の後ろに .then() を加えることで、成功時にも、失敗時にも常に実行される Finally のような処理を追加することができます。
function sample_finally() { aFunc3(100).then((data) => { console.log(data); return aFunc3(data); }) .then((data) => { console.log(data); return aFunc3(data); }) .then((data) => { console.log(data); }) .catch((e) => { console.log(e); }) .then(() => { console.log('*** Finally ***'); }); }
ES2018(ES9) では、.finally() がサポートされました。
function sample_finally2() { aFunc3(100).then((data) => { console.log(data); return aFunc3(data); }) .then((data) => { console.log(data); return aFunc3(data); }) .then((data) => { console.log(data); }) .catch((e) => { console.log("catch"); console.log(e); }) .finally(() => { console.log('*** Finally ***'); }); }
Promise.all() は配列で指定されたすべての Promise タスクを待ち合わせ、すべてのタスクが完了した時点で .then() のコールバック関数を呼び出します。
function taskA() { return new Promise((callback) => { console.log("taskA start."); setTimeout(function() { console.log("taskA end."); callback(); }, Math.random() * 3000); }); } function taskB() { return new Promise((callback) => { console.log("taskB start."); setTimeout(function() { console.log("taskB end."); callback(); }, Math.random() * 3000); }); } function sample_all() { p1 = taskA(); p2 = taskB(); Promise.all([p1, p2]).then(() => { console.log("taskA and taskB are finished."); }); }
Promise.race() は配列で指定された Promise タスクを待ち合わせ、いずれかひとつのタスクが完了した時点で、.then() のコールバック関数を呼び出します。
function sample_race() { p1 = taskA(); p2 = taskB(); Promise.race([p1, p2]).then(() => { console.log("taskA or task B is finished."); }); }
ES2021 では Promise.any() が追加されました。Promise.race() と同様、いずれかのタスクの完了を待ちますが、race() がいずれかのタスクが成功(resolve)または失敗(reject)した時点で終了するのに対し、any() はいずれかのタスクが成功(resolve)した時のみ終了します。
function sample_race() { p1 = taskA(); p2 = taskB(); Promise.any([p1, p2]).then(() => { console.log("taskA or task B is finished."); }); }
Promise.all() では、指定したタスクのいずれか一つがエラーになるとそこで待ち合わせを完了してしまいますが、ES2020 でサポートされた Promise.allSettled() を用いると、タスクがエラーとなっても、すべてのタスクが成功終了するか、エラー終了するまで、処理を待ち合わせることが可能となります。
p1 = Promise.resolve("OK1"); p2 = Promise.reject("NG2"); p3 = Promise.resolve("OK3"); Promise.allSettled([p1, p2, p3]).then( resolveList => resolveList.forEach(res => console.log(res)), rejectList => rejectList.forEach(rej => console.log(rej)) ); // => {status: "fulfilled", value: "OK1"} // => {status: "rejected", reason: "NG2"} // => {status: "fulfilled", value: "OK3"}
ES2017 では、Promise に加え、async/await がサポートされました。こちらも、Internet Explorer を除く大半のモダンブラウザで利用可能です。async と await を用いることで、Promise に対応した非同期関数を、同期関数の様に呼び出すことが可能となります。同期関数の様に呼び出したい非同期関数を呼び出す際に await をつけます。await を呼び出す関数に async をつけます。
async function sample_async_await() { var val = 100; val = await aFunc2(val); console.log(val); // 200 val = await aFunc2(val); console.log(val); // 400 val = await aFunc2(val); console.log(val); // 800 }
エラー処理に対応するコードは下記の様になります。
async function sample_async_await_with_catch() { var val = 100; try { val = await aFunc3(val); console.log(val); val = await aFunc3(val); console.log(val); val = await aFunc3(val); console.log(val); } catch (e) { console.log(e); } }
await を呼び出す関数には async をつける必要がありますが、ES2022 からトップレベルからの呼び出しのみは async 関数でラップしなくても await を呼び出せるようになりました。詳細は「トップレベル await」を参照してください。
ES2018(ES9) では、非同期なイテラブルオブジェクトに対して、for await (... of ...) でループを回せるようになりました。例えば、サーバからデータを1件ずつ読み込む非同期処理を、同期処理の様に for ループで記述することが可能となります。
var asyncIterableObject = {
[Symbol.asyncIterator]() {
return {
count: 0,
next() {
return new Promise((callback) => {
setTimeout(() => {
callback({
value: this.count,
done: (this.count < 5) ? false : true,
})
this.count++;
}, Math.random() * 1000);
});
}
};
}
};
async function for_await_of() {
for await (num of asyncIterableObject) {
console.log(num); // 0, 1, 2, 3, 4
}
};