astris.design

[JS]非同期処理とPromise then/catch/finally, async/await, all/allSettled/race

JavaScriptには、非同期処理をスマートに記述するPromiseという仕組みがあります。非同期処理をPromiseベースで実装する方法について、解説します(サンプルあり)。

【目次・非同期処理とPromise】

非同期処理とは

非同期処理とは、ある処理の完了を待たずに次の処理に進むことです。
プログラミングの処理の多くは、ある処理が完了してから次の処理に進む、同期処理です。
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となります。

promise.js
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(() => 後処理);

サンプル

promise-then.js
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(() => 後処理);

サンプル

promise-all.js
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;
  ...
}

サンプル

async-await.js
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:非同期処理を同期処理のように記述