TypeScriptで非同期処理の失敗をリトライ(再試行)する方法を初心者向けに徹底解説!
生徒
「TypeScriptで通信プログラムを作っているのですが、たまにインターネットの調子が悪くてエラーになってしまいます。失敗したときにもう一度自動でやり直す方法はありますか?」
先生
「それはリトライ処理と呼ばれる技術ですね。TypeScriptでは、Promiseやasync/awaitといった非同期処理の仕組みを使って、失敗したときに再度実行するプログラムを簡単に書くことができますよ。」
生徒
「リトライ処理って難しそうですが、初心者でも実装できますか?」
先生
「大丈夫です!基本的な構造さえ理解すれば、誰でも書けるようになります。まずは非同期処理の基本から、具体的な再試行のコードまで順番に学んでいきましょう!」
1. 非同期処理とPromiseの基本を知ろう
プログラミングを始めたばかりの方にとって、まず高い壁となるのが非同期処理という言葉です。通常、プログラムは上から下へと順番に実行されます。しかし、インターネットを通じてデータを取得する場合などは、データの到着を待っている間、他の作業が止まってしまうと困ります。そこで登場するのが非同期処理です。
TypeScriptにおけるPromise(プロミス)とは、「今はまだ結果が出ていないけれど、将来的に成功か失敗の結果を返します」という約束のような仕組みです。Promiseには以下の三つの状態があります。
- Pending(待機):結果を待っている状態です。
- Fulfilled(成功):無事に約束が果たされ、データが得られた状態です。
- Rejected(失敗):何らかの理由で約束が守れず、エラーになった状態です。
この失敗した状態(Rejected)になったときに、諦めずにもう一度実行するのが今回のテーマであるリトライ処理です。たとえば、スマートフォンの電波が一時的に悪くなったときに、自動で再読み込みをしてくれるような機能を作成するイメージです。これにより、ユーザーがいちいち手動で更新ボタンを押さなくても済むようになり、使い勝手の良いアプリケーションを作成することができます。
2. asyncとawaitでコードを読みやすくする
以前のプログラムではPromiseを扱うのが大変でしたが、現代のTypeScriptではasync(エイシンク)とawait(アウェイト)というキーワードを使います。これを使うと、非同期な処理をまるで普通の順番通りの処理のように書くことができるため、初心者の方でもコードが読みやすくなります。
asyncは関数の前に付けて、「この関数の中では非同期処理を使いますよ」という宣言です。そして、awaitは「非同期処理が終わるまでここで少し待ってください」という命令になります。リトライ処理を作る際も、この二つを組み合わせることで、エラーが起きたら少し待ってから再度実行するという流れを直感的に記述できるようになります。
まずは、簡単な非同期処理の例を見てみましょう。このコードは、指定した秒数だけ待機するだけのシンプルなものです。
async function waitProcess() {
console.log("処理を開始します");
// 2秒間待機する処理
await new Promise(resolve => setTimeout(resolve, 2000));
console.log("2秒経過しました");
}
waitProcess();
実行結果は以下のようになります。
処理を開始します
(2秒待機)
2秒経過しました
3. try-catch構文でエラーを捕まえる
リトライ処理を実装するためには、プログラムが「失敗した」ことを検知しなければなりません。そこで使われるのがtry-catch(トライ・キャッチ)構文です。これは「とりあえずやってみて(try)、もし失敗してエラーが出たら捕まえる(catch)」という仕組みです。
プログラムの中でエラーが発生すると、通常はその時点でアプリが停止してしまいます。しかし、try-catchを使えば、エラーが起きた後の動きを自分でコントロールできます。リトライ処理の場合は、catchの中で「もう一回やってみる?」という判断を挟むことになります。パソコンを触ったことがない方にとって、エラーは怖いものに感じるかもしれませんが、プログラミングにおいてエラーは「次に何をすべきか教えてくれる道標」のようなものです。適切にエラーを処理することで、頑丈なプログラムを作ることができます。
4. ループ文を使った基本的なリトライ処理の実装
それでは、いよいよ本題のリトライ処理を実装してみましょう。最も分かりやすい方法は、for文やwhile文といった繰り返し処理の中で、成功するまでtryを繰り返す方法です。回数を指定してリトライすることで、無限に繰り返してパソコンに負荷をかけすぎるのを防ぐことができます。
下記のサンプルコードでは、最大3回まで通信を試みるプログラムを作成しています。わざと失敗させるために、ランダムでエラーを発生させる仕組みを入れています。実際の開発では、ここが「サーバーへのデータ取得」などに置き換わります。
async function fetchDataWithRetry() {
const maxAttempts = 3; // 最大で3回まで試す
for (let i = 1; i <= maxAttempts; i++) {
try {
console.log(i + "回目の試行中です...");
// 50%の確率で失敗させるシミュレーション
if (Math.random() < 0.5) {
throw new Error("通信エラーが発生しました");
}
console.log("データの取得に成功しました!");
return; // 成功したので関数を終了する
} catch (error) {
console.log("エラーが起きました: " + i + "回目");
if (i === maxAttempts) {
console.log("すべての試行に失敗しました。諦めます。");
}
}
}
}
fetchDataWithRetry();
実行結果(失敗が続いた場合)は以下のようになります。
1回目の試行中です...
エラーが起きました: 1回目
2回目の試行中です...
エラーが起きました: 2回目
3回目の試行中です...
エラーが起きました: 3回目
すべての試行に失敗しました。諦めます。
5. 待機時間を入れる「ウェイトリトライ」の重要性
前章のリトライ処理は、失敗した瞬間にすぐ次の試行を開始してしまいます。しかし、ネットワークエラーの場合、数ミリ秒後にやり直してもまだ状況が改善していないことが多いです。そこで、失敗した後に少し時間を置いてから再試行するウェイト処理を入れるのが一般的です。
これをプログラミング用語で「スリープ」や「ディレイ」と呼びます。人間も、電話が繋がらなかったときに一呼吸置いてからかけ直しますよね。それと同じことをプログラムにもさせてあげましょう。TypeScriptでは、setTimeoutという標準機能をPromiseで包むことで、awaitを使って簡単に待機時間を挟むことができます。
// 指定した時間(ミリ秒)だけ待機する関数
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
async function smartRetry() {
const retryLimit = 3;
for (let count = 1; count <= retryLimit; count++) {
try {
console.log(count + "回目の挑戦...");
// ここでエラーが起きる可能性のある処理を行う
throw new Error("一時的な故障");
} catch (err) {
if (count < retryLimit) {
console.log("失敗。1秒待ってからやり直します。");
await sleep(1000); // 1秒(1000ミリ秒)待つ
} else {
console.log("ダメでした。管理者にお問い合わせください。");
}
}
}
}
smartRetry();
6. 指数バックオフという高度なテクニック
本格的なアプリ開発では、指数バックオフ(Exponential Backoff)という手法がよく使われます。これは「1回目は1秒待つ、2回目は2秒待つ、3回目は4秒待つ」というように、失敗するたびに待ち時間を倍にしていく方法です。なぜこんなことをするのでしょうか?
理由は、サーバーへの負荷を減らすためです。もしサーバーがパンクして動かなくなっているときに、世界中のパソコンが猛スピードでリトライを繰り返すと、サーバーはさらに壊れてしまいます。待ち時間をどんどん延ばすことで、サーバーが復活するまでの時間稼ぎをしつつ、無駄な攻撃を防ぐという優しさが込められた技術なのです。初心者の方でも、この考え方を知っているだけで「おっ、詳しいな」と思われますよ。
数式で書くと難しく見えますが、プログラムでは現在の試行回数を使って計算するだけなので、実装は意外と簡単です。
async function advancedRetry() {
let currentTrial = 1;
const maxTrial = 4;
while (currentTrial <= maxTrial) {
try {
console.log("アクセス試行: " + currentTrial);
throw new Error("サーバー混雑");
} catch (e) {
if (currentTrial === maxTrial) {
console.log("最大回数に達しました。終了します。");
break;
}
// 2の(試行回数)乗ミリ秒待つ計算
const waitTime = Math.pow(2, currentTrial) * 1000;
console.log(waitTime + "ミリ秒待機します...");
await new Promise(r => setTimeout(r, waitTime));
currentTrial++;
}
}
}
advancedRetry();
実行結果は以下のようになります。
アクセス試行: 1
2000ミリ秒待機します...
アクセス試行: 2
4000ミリ秒待機します...
アクセス試行: 3
8000ミリ秒待機します...
アクセス試行: 4
最大回数に達しました。終了します。
7. リトライ処理を作る際の注意点
便利なリトライ処理ですが、何でもかんでもやり直せば良いというわけではありません。注意すべきポイントがいくつかあります。
まず一つ目は、「絶対に成功しないエラー」をリトライしないことです。たとえば、ログインパスワードが間違っている場合や、URLがそもそも存在しない場合は、何度やり直しても結果は変わりません。こうしたエラーのときはすぐに諦めて、ユーザーに間違いを伝えるのが正しいプログラムの動きです。エラーの種類(ステータスコードなど)を見て、リトライすべきかどうかを判断する条件分岐を入れましょう。
二つ目は、「無限ループ」に陥らないことです。回数制限を忘れてしまうと、プログラムが永遠に失敗とリトライを繰り返し、パソコンのバッテリーを激しく消耗させたり、通信費が膨大になったりする危険があります。必ず「最大で何回まで」という出口を作っておくことが、プログラマーとしてのマナーです。
三つ目は、「ユーザーへの表示」です。裏側でこっそりリトライしている間、画面が全く動かないとユーザーは「フリーズしたのかな?」と不安になります。「通信に失敗しました。再試行しています...」といったメッセージや、くるくる回る読み込みアイコンを表示して、今の状況を親切に教えてあげることが大切です。これらは「ユーザー体験(UX)」を向上させるために非常に重要な要素となります。
8. 汎用的なリトライ関数の作成
毎回リトライのコードを書くのは大変です。そこで、どんな処理でもリトライできるように共通の関数を作っておくと便利です。TypeScriptのGenerics(ジェネリクス)という機能を使うと、どんな型のデータが返ってくる処理でも使い回せる最強のリトライツールを作ることができます。少し難しい内容ですが、これが使いこなせれば中級者への仲間入りです!
ジェネリクスは、データ型を「変数」のように扱う仕組みです。どんな関数でも受け取って、それを指定回数分リトライしてくれる便利なテンプレートのようなものです。以下のコードは、実務でも使えるような設計になっています。
/**
* リトライを実行する共通関数
* @param task 実行したい非同期処理
* @param retries 最大リトライ回数
* @param delay 待機時間(ミリ秒)
*/
async function executeWithRetry<T>(
task: () => Promise<T>,
retries: number = 3,
delay: number = 1000
): Promise<T> {
try {
return await task();
} catch (error) {
if (retries <= 0) {
throw error;
}
console.log("再試行まで残り: " + retries + "回");
await new Promise(resolve => setTimeout(resolve, delay));
return executeWithRetry(task, retries - 1, delay);
}
}
// 実際の使い方例
async function myWork() {
console.log("APIを呼び出します...");
throw new Error("ネットワークエラー");
}
executeWithRetry(myWork, 2, 2000).catch(() => {
console.log("最終的に失敗しました");
});
この関数を使えば、他の場所で通信プログラムを書いたときも「この処理を3回リトライしてね」と一行書くだけで済むようになります。コードの再利用性を高めることは、効率的なプログラミングの第一歩です。
9. 実践的なライブラリの紹介
自分でリトライ処理を書く方法を学びましたが、実は世界中のプログラマーが作った便利なライブラリも存在します。たとえば、有名な「axios(アクシオス)」という通信用ライブラリには、設定一つでリトライができるプラグインがあったりします。また、「retry-ts」というTypeScript専用のライブラリもあります。
「自分で書けるのにどうしてライブラリを使うの?」と思うかもしれません。ライブラリは、多くの人の目でチェックされているためバグが少なく、非常に高機能です。しかし、中身の仕組み(今回学んだようなasync/awaitやtry-catchの仕組み)を知らずにライブラリを使うと、トラブルが起きたときに対処できません。まずは自分で書いてみて、仕組みを理解した上で便利な道具を活用するのが、上達への近道です。
TypeScriptの世界には、他にもたくさんの便利な機能があります。非同期処理をマスターすれば、チャットアプリ、天気予報アプリ、SNSなど、リアルタイムでデータをやり取りする楽しいプログラムが作れるようになります。今回のリトライ処理はその大きな一歩です。焦らず一つずつ覚えていきましょう。