Promise と async・await でリトライ処理を実装する

通信処理なんかが Promise で実装されている時に、自前でリトライ処理をやらないといけなくなった。

巷にはどんなやり方があるのか、Promise のまま扱う場合と、asyncawait で扱う場合とを調べてみた。

実行環境は、特にトランスパイルなど行わず、素の Node.js を使用。

Promise でリトライ処理

// 失敗したらリトライさせたい処理
const myFunc = () => {
  return new Promise((resolve, reject) => {
    if(Math.random() < .5) {
      console.log('Resolve');
      resolve('OK');
    }
    else {
      console.log('Reject');
      reject('Error');
    }
  });
};

// 最大3回リトライする
myFunc()
  .catch(myFunc)
  .catch(myFunc)
  .then((result) => {
    console.log('成功しました : ', result);
  })
  .catch((error) => {
    console.log('失敗しました : ', error);
  });

// 次のように書いても OK
Promise.reject()
  .catch(myFunc)
  .catch(myFunc)
  .catch(myFunc)
  .then((result) => {
    console.log('成功しました : ', result);
  })
  .catch((error) => {
    console.log('失敗しました : ', error);
  });

asyncawait でリトライ処理

// 失敗したらリトライさせたい処理 (先程と同じ)
const myFunc = () => {
  return new Promise((resolve, reject) => {
    if(Math.random() < .5) {
      console.log('Resolve');
      resolve('OK');
    }
    else {
      console.log('Reject');
      reject('Error');
    }
  });
};

// await を使うため async を指定して即時関数で実行する
(async () => {
  const maxRetry = 3;  // 最大3回リトライする
  let result;  // 正常終了時の結果を格納する変数
  
  for(let retryCount = 0; retryCount < maxRetry; retryCount++) {
    try {
      result = await myFunc();
    }
    catch(error) {
      console.log(`${retryCount + 1} 回目の失敗`, error);
    }
    
    if(result) {
      break;  // 正常終了していれば for ループを抜ける
    }
  }
  
  if(result) {
    console.log('成功しました : ', result);
  }
  else {
    console.log('失敗しました');
    throw new Error('3回リトライしたが失敗');  // 状況に応じて例外をスローしたり…
  }
})();

// エラー情報が後で必要なら、変数 result と同じ要領で蓄えておけば良い
// また、`myFunc()` が戻り値を返さない関数である場合は、フラグ変数にする
(async () => {
  const maxRetry = 3;
  let isSucceed = false;  // 正常終了したかどうかを確認するフラグ変数
  const errors = [];  // エラー情報を蓄える
  
  for(let retryCount = 0; retryCount < maxRetry; retryCount++) {
    try {
      await myFunc();  // 戻り値を使用しない場合
      isSucceed = true;  // 成功のフラグを立てる
    }
    catch(error) {
      console.log(`${retryCount + 1} 回目の失敗`, error);
      errors.push(error);  // エラー情報を追加する
    }
    
    if(isSucceed) {
      break;
    }
  }
  
  if(isSucceed) {
    // 最終的に成功したが、それまでにリトライしていた場合は errors に最高2件のエラー情報が追加されることになる
    console.log('成功しました', errors);
  }
  else {
    // 3回とも失敗した場合は、3件のエラー情報が格納されている
    console.log('失敗しました', errors);
  }
})();

asyncawait を使用する場合は、「非同期処理」であることを忘れて、「for ループ中に条件を満たしたら break する」という最も基礎的な文法で解決できる。for だと決まった回数だけループしそうな感じがあるので、while で書いても良いだろう。

ESLint を入れていると、no-await-in-loop という警告が出るかもしれない。コレは、for ループ内で await を使って非同期処理を待っていることで、結果的に直列実行になっているのを警告してくれている。ただ、このようなリトライ時の処理は並列実行されては困るので、直列実行すべき対象として、適宜 ESLint エラーを回避しておこう。

以上

上述の例は、「通信エラー時」のリトライ処理によくある、「再実行まで数秒待機する」といったリトライ間隔の調整用コードが入っていない。どちらの書き方を使うか、どのようにリトライさせるかは、目的にあわせて調整しよう。