Promise と async・await でリトライ処理を実装する
通信処理なんかが Promise で実装されている時に、自前でリトライ処理をやらないといけなくなった。
巷にはどんなやり方があるのか、Promise のまま扱う場合と、async
・await
で扱う場合とを調べてみた。
実行環境は、特にトランスパイルなど行わず、素の 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);
});
async
・await
でリトライ処理
// 失敗したらリトライさせたい処理 (先程と同じ)
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);
}
})();
async
・await
を使用する場合は、「非同期処理」であることを忘れて、「for
ループ中に条件を満たしたら break
する」という最も基礎的な文法で解決できる。for
だと決まった回数だけループしそうな感じがあるので、while
で書いても良いだろう。
ESLint を入れていると、no-await-in-loop
という警告が出るかもしれない。コレは、for
ループ内で await
を使って非同期処理を待っていることで、結果的に直列実行になっているのを警告してくれている。ただ、このようなリトライ時の処理は並列実行されては困るので、直列実行すべき対象として、適宜 ESLint エラーを回避しておこう。
- 参考 : ES7 async/await でのエラーハンドリング - おなか周りの脂肪がやばい
- 参考 : no-await-in-loop - Rules - ESLint - Pluggable JavaScript linter
以上
上述の例は、「通信エラー時」のリトライ処理によくある、「再実行まで数秒待機する」といったリトライ間隔の調整用コードが入っていない。どちらの書き方を使うか、どのようにリトライさせるかは、目的にあわせて調整しよう。