[JS]非同期処理とPromise then/catch/finally, async/await, all/allSettled/race
JavaScriptには、非同期処理をスマートに記述するPromiseという仕組みがあります。非同期処理をPromiseベースで実装する方法について、解説します(サンプルあり)。
- 非同期処理とは
- Promise:非同期オブジェクト
- then, catch, finally:Promiseチェーン
- all, allSettled, race:Promiseの並行処理
- async, await:非同期処理を同期処理のように記述
- まとめ
非同期処理とは
非同期処理とは、ある処理の完了を待たずに次の処理に進むことです。
プログラミングの処理の多くは、ある処理が完了してから次の処理に進む、同期処理です。
JavaScriptはその役割上、非同期処理を実装することが多い言語です。
例えば、ネットワーク通信やUIイベント、ファイルの読み込みなどを、非同期処理として扱う必要があります。
そのため、非同期処理に慣れることは、JavaScriptをマスターする上で欠かせません。
非同期処理は元々、コールバック関数を利用して記述されていましたが、非同期処理が増えるほどコールバックの入れ子が深くなり、コードの可読性が低下するという問題(コールバック地獄)がありました。
それを解決するために、Promiseという仕組みがES2015で規定されました。
ブラウザにもNode.jsにも、Promiseベースの非同期APIが、用意されています。
例えば、HTTPおよびHTTPSリクエストを送信するfetch APIも、その一つです。
Promiseベースの実装ができるようになるには、まず、Promiseオブジェクトについて理解する必要があります。
Promise:非同期オブジェクト
Promiseは、非同期処理の実行状況を管理するオブジェクトです。
Promiseオブジェクトは、非同期処理の実行状態に応じて、以下の3つの状態を取ります。
- pending:保留中
- fullfilled:成功
- rejected:失敗
処理が完了して成功か失敗になっている状態を「settled:完了」と呼ぶこともあります。
後述するthen/catch/finallyメソッドにより、Promiseチェーンを記述することで、可読性の高い非同期処理を実装することができます。
Promiseベースの非同期APIを作成するには、Promiseコンストラクタを使用します。
関数内で、Promiseオブジェクトを生成し、戻り値として返すよう記述します。
非同期処理の結果をresolve関数に渡し、エラー情報をreject関数に渡すことで、Promiseチェーン上で利用することができます。
function 非同期関数( 引数 ) {
return new Promise((resolve, reject) => {
if ( 条件式 ) {
resolve( 処理結果 );
} else {
reject( エラー情報 );
}
});
}
サンプル
コールバック系の非同期APIであるsetTimeoutを、Promiseでラップしています。
console.logメソッドが呼び出されるタイミングでは、非同期関数asyncWaitの処理は完了していないので、戻り値のPromiseオブジェクトの状態は、pendingとなります。
function asyncWait(time) {
return new Promise((resolve, reject) => {
if (time < 5001) {
setTimeout(() => resolve(time), time);
} else {
setTimeout(() => reject(new Error('Over 5sec')), 1000);
}
});
}
console.log(asyncWait(3000));
//「Promise { <pending> }」
then, catch, finally:Promiseチェーン
Promiseベースの非同期処理は、戻り値のPromiseオブジェクトからthen/catch/finallyメソッドを呼び出す形式で、後続の処理を記述します。
これらのメソッドもPromiseオブジェクトを返すため、メソッドを介して、処理をつなげていくことができます。
この記法はPromiseチェーンと呼ばれ、入れ子が深くなるコールバックによる非同期処理に比べて、可読性が向上します。
Promise完了後の処理は、thenメソッドに渡すコールバック関数として、記述します。
1つ目のコールバック関数の処理は、Promiseの処理が成功した場合に実行されます。非同期処理の結果(resolve関数の引数)を受け取り、正常処理を記述します。
2つ目のコールバック関数の処理は、Promiseの処理が失敗した場合に実行されます。非同期処理のエラー情報(reject関数の引数)を受け取り、例外処理を記述します。2つ目のコールバック関数は省略することができます。
また、例外処理は、catchメソッドに記述することもできます。
finallyメソッドの処理は、Promiseが成功した場合も失敗した場合も実行されます。引数は取らず、リソースの解放などの後処理を記述します。
catchメソッドとfinallyメソッドは省略することができます。
非同期関数( 引数 )
.then((res) => 正常処理)
.catch((err) => 例外処理)
.finally(() => 後処理);
非同期関数( 引数 )
.then((res) => 正常処理, (err) => 例外処理)
.finally(() => 後処理);
サンプル
function asyncWait(time) {
return new Promise((resolve, reject) => {
if (time < 5001) {
setTimeout(() => resolve(`${time/1000}sec passed`), time);
} else {
setTimeout(() => reject(new Error('Over 5sec')), 1000);
}
});
}
asyncWait(3000)
.then((res) => console.log(res)) //「3sec passed」
.catch((err) => console.log(err))
.finally(() => console.log('Completed!')); //「Completed!」
asyncWait(6000)
.then((res) => console.log(`${res / 1000}sec passed`))
.catch((err) => console.log(err)) //「Error: Over 5sec」
.finally(() => console.log('Completed!')); //「Completed!」
all, allSettled, race:Promiseの並行処理
Promiseベースの非同期処理は、Promiseチェーンによって処理を同期的につなげるだけでなく、複数の非同期処理を並行して扱うこともできます。
Promiseオブジェクトには、複数の非同期処理を並行処理し、その結果を集約するための静的メソッドが用意されています。
これらのメソッドは、Promiseオブジェクトの配列を受け取り、Promiseオブジェクトを返します。
Promise.allメソッドは、全てのPromiseが成功した場合、成功のPromiseを配列として返します。
Promiseの一つが失敗した場合は、その時点で、失敗のPromiseを返します。
Promise.allSettledメソッドは、全てのPromiseが完了した時、成功のPromiseを返します。
失敗したPromiseがある場合でも、全てのPromiseが完了するまで処理を行い、成功/失敗の結果を配列として返します。
allSettledメソッドは、ES2020に規定されました。
Promise.raceメソッドは、Promiseが一つ完了した時点で、完了のPromiseを返します。
Promiseの成功/失敗は、最初に完了したPromiseの結果となります。
Promise.all([非同期関数1, 非同期関数2, ...])
.then((res) => 正常処理)
.catch((err) => 例外処理)
.finally(() => 後処理);
サンプル
function asyncWait(time) {
return new Promise((resolve, reject) => {
if (time < 5001) {
setTimeout(() => resolve(`${time/1000}sec passed`), time);
} else {
setTimeout(() => reject(new Error('Over 5sec')), 1000);
}
});
}
Promise.race([asyncWait(5000), asyncWait(1000), asyncWait(2000)])
.then((res) => console.log(res)) //「1sec passed」
.catch((err) => console.log(err))
.finally(() => console.log('Completed!')); //「Completed!」
Promise.all([asyncWait(5000), asyncWait(1000), asyncWait(2000)])
.then((res) => console.log(res)) //「[ '5sec passed', '1sec passed', '2sec passed' ]」
.catch((err) => console.log(err))
.finally(() => console.log('Completed!')); //「Completed!」
Promise.allSettled([asyncWait(6000), asyncWait(1000), asyncWait(2000)])
.then((res) => console.log(res))
/*「[
{
status: 'rejected',
reason: Error: Over 5sec
at Timeout._onTimeout (C:\...\promise-all.js:6:31)
at listOnTimeout (node:internal/timers:564:17)
at process.processTimers (node:internal/timers:507:7)
},
{ status: 'fulfilled', value: '1sec passed' },
{ status: 'fulfilled', value: '2sec passed' }
]」*/
.finally(() => console.log('Completed!')); //「Completed!」
async, await:非同期処理を同期処理のように記述
async/awaitキーワードを使用することで、Promiseベースの非同期処理をより簡潔に記述することができます。
asyncキーワードで修飾された関数は、戻り値がPromiseオブジェクトになります(関数内でのPromiseの生成は不要)。
asyncキーワードで修飾された関数内でだけ、awaitキーワードを使用することができます。
await式を使用すると、非同期処理を同期処理のように記述することができます。
awaitキーワードは、Promiseオブジェクトを受け取り、戻り値や例外に変換します。
await式は、処理をブロックせず、Promiseが完了するまで待ちます。
async/awaitで非同期処理を記述する場合、例外処理はtry/catch/finally文で記述します。async/awaitキーワードは、ES2017で規定されました。
async function 非同期関数() {
let async_v1 = await Promiseベースの非同期処理1;
let async_v2 = await Promiseベースの非同期処理2;
...
}
サンプル
function asyncWait(time) {
return new Promise((resolve, reject) => {
if (time < 5001) {
setTimeout(() => resolve(`${time/1000}sec passed`), time);
} else {
setTimeout(() => reject(new Error('Over 5sec')), 1000);
}
});
}
async function asyncCount(num, time) {
try {
for (let i = 0; i < num; i++) {
const count = await asyncWait(time);
console.log(`${i}: ${count}`);
//「0: 1sec passed」「1: 1sec passed」「2: 1sec passed」
}
} catch (err) {
console.log(err);
} finally {
console.log('Complete!');
}
}
asyncCount(3, 1000);
まとめ
JavaScriptの非同期処理とPromiseについて、解説しました。
Promiseベースの非同期処理の実装は、以下のようなパターンがあります。
- then, catch, finally:Promiseチェーン
- all, allSettled, race:Promiseの並行処理
- async, await:非同期処理を同期処理のように記述