TypeScriptでカスタムエラークラスを使ったリトライ条件の制御方法

TypeScriptでは、エラーハンドリングの際に、デフォルトのエラー処理に加えて、カスタムエラークラスを利用することで、より柔軟で詳細なエラーメッセージや制御を行うことができます。特にリトライ処理が求められるケースでは、エラーの種類に応じて処理を分岐させ、適切な対応を取ることが重要です。本記事では、TypeScriptでカスタムエラークラスを用いて、どのようにリトライ条件を制御できるかを解説します。リトライ処理の実装は、信頼性の高いアプリケーションを作る上で欠かせない要素です。

目次

カスタムエラークラスとは

カスタムエラークラスとは、JavaScriptやTypeScriptで標準的に提供されているErrorクラスを継承し、独自のエラーメッセージやプロパティを追加したエラーのことを指します。通常のエラーでは特定の条件に基づくエラーハンドリングが難しい場合、カスタムエラーを作成することで、より詳細なエラー情報を提供し、エラーの種類に応じた柔軟な対応が可能となります。

なぜカスタムエラーを使うのか

カスタムエラーを使うことで、次のようなメリットがあります。

  • エラーの種類を明確化し、処理の分岐がしやすくなる
  • リトライが必要なエラーとそうでないエラーを区別できる
  • 特定の条件に応じたエラーメッセージやデバッグ情報を追加できる

TypeScriptでは、型情報を活用してエラー処理をより厳密に行えるため、カスタムエラーの利用は非常に効果的です。

カスタムエラーを使ったリトライの必要性

アプリケーション開発において、エラーが発生した際に自動で再試行(リトライ)する仕組みは非常に重要です。しかし、すべてのエラーに対してリトライを行うと、無限ループや不必要な処理が発生する可能性があります。そこで、エラーの種類に応じてリトライするかどうかを判断する必要があり、カスタムエラーを使用することでこれを効果的に実装できます。

リトライが必要な場面

次のような状況でリトライが有効です。

  • ネットワークの一時的な接続不良
  • 外部APIのレスポンスが遅延している場合
  • サーバー側で一時的な問題が発生している場合

これらのケースでは、エラーをカスタムエラーとして分類することで、そのエラーがリトライ可能かどうかを明示的に判断し、処理の制御が可能になります。

リトライ不要なケース

一方で、次のようなケースではリトライは不要です。

  • 誤ったユーザー入力によるエラー
  • ファイルやリソースの恒久的な欠如
  • 権限不足によるアクセス拒否

このようなエラーではリトライを行わないほうが望ましく、カスタムエラーを用いることで、リトライ不要なエラーを適切に区別できるようにします。

カスタムエラーの作成手順

TypeScriptでカスタムエラーを作成する手順は、標準のErrorクラスを拡張することで行います。これにより、独自のプロパティやメソッドを追加して、リトライ条件に合ったエラー処理が可能となります。以下はカスタムエラーを作成する際の基本的な流れです。

カスタムエラークラスの定義

まずは、Errorクラスを継承してカスタムエラーを定義します。たとえば、ネットワーク接続エラーを表すNetworkErrorクラスを作成します。

class NetworkError extends Error {
  constructor(message: string) {
    super(message); // 親クラスのErrorコンストラクタを呼び出す
    this.name = "NetworkError"; // エラーの名前を指定
  }
}

このNetworkErrorクラスは、エラーが発生した際にそのエラーがネットワーク関連であることを明示的に示し、リトライ可能なエラーとして扱うことができます。

カスタムプロパティの追加

さらに、リトライ回数やエラー発生のタイムスタンプなど、独自の情報を追加したい場合は、カスタムプロパティを追加できます。

class NetworkError extends Error {
  retryCount: number; // リトライ回数のトラッキング
  timestamp: Date; // エラー発生時刻

  constructor(message: string, retryCount: number = 0) {
    super(message);
    this.name = "NetworkError";
    this.retryCount = retryCount;
    this.timestamp = new Date();
  }
}

これにより、リトライの際に現在のリトライ回数を追跡し、条件に応じた制御が容易になります。

エラーハンドリングでのカスタムエラーの利用

カスタムエラーを利用してエラーハンドリングを行う場合、try-catchブロックでエラーをキャッチし、カスタムエラーに基づいた処理を行います。

try {
  // エラーが発生する可能性のある処理
  throw new NetworkError("ネットワーク接続に失敗しました");
} catch (error) {
  if (error instanceof NetworkError) {
    console.log(`エラーが発生しました: ${error.message}`);
    console.log(`リトライ回数: ${error.retryCount}`);
  } else {
    console.error("予期しないエラーが発生しました", error);
  }
}

これにより、カスタムエラーを使ってエラーをより細かく制御し、リトライの条件を決めることが可能です。

特定エラーに基づいたリトライロジックの構築

カスタムエラーを作成した後、そのエラーが発生した際にリトライするかどうかを判断するロジックを組み込むことができます。特定のエラーに基づいてリトライを行うことで、無駄な処理を避け、システムの信頼性を向上させることが可能です。

リトライ対象のエラーの識別

まず、リトライ対象となるエラーの種類を特定します。たとえば、ネットワークの一時的なエラーであればリトライを行いますが、ユーザーの入力ミスやシステム内部の致命的なエラーであればリトライは不要です。以下のように、特定のエラーが発生した場合にのみリトライを行うロジックを組み込みます。

function shouldRetry(error: Error): boolean {
  // ネットワーク関連エラーや一時的なサーバーエラーが対象
  return error instanceof NetworkError || error instanceof TimeoutError;
}

この関数は、エラーが特定のカスタムエラー(NetworkErrorTimeoutError)の場合にのみtrueを返し、それ以外のエラーではfalseを返します。

リトライロジックの実装

リトライが必要と判断された場合、実際にリトライ処理を行うロジックを実装します。以下は、指定されたリトライ回数まで処理を再試行する簡単な例です。

async function executeWithRetry(operation: () => Promise<void>, retries: number): Promise<void> {
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      await operation(); // 実行したい処理
      return; // 成功したら終了
    } catch (error) {
      if (shouldRetry(error) && attempt < retries) {
        console.log(`リトライ中... (${attempt + 1}/${retries})`);
      } else {
        throw error; // リトライ不可、またはリトライ回数超過時にエラーを再スロー
      }
    }
  }
}

このコードでは、指定されたリトライ回数を超えるまでリトライを行います。もし、shouldRetry関数でリトライ対象と判断されたエラーが発生した場合にのみリトライを行い、回数を超えるとエラーをスローします。

エラーハンドリングの最適化

カスタムエラーを使用することで、リトライが必要なケースと不要なケースを明確に分けることができ、リトライを効果的に行えます。例えば、NetworkErrorTimeoutErrorなどの一時的なエラーに対してリトライを行い、InvalidInputErrorのような永続的なエラーにはリトライを行わない設計にすることが可能です。

このように、カスタムエラーに基づいたリトライロジックを組み込むことで、システム全体のエラーハンドリングを柔軟かつ効率的に制御できます。

実際にリトライを実装するためのコード例

カスタムエラーを使ったリトライロジックの具体的な実装を見ていきます。ここでは、ネットワークの不安定な状況で外部APIへのリクエストを行い、失敗した場合にリトライする例を紹介します。リトライ処理の基盤となるのは、以前作成したカスタムエラーとリトライ判定ロジックです。

基本的なリトライ処理の実装例

まず、外部APIへのリクエストを行う関数を定義し、それをリトライするためのロジックを組み込みます。ネットワーク関連のエラーが発生した場合には、指定された回数だけリトライを試みます。

class NetworkError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "NetworkError";
  }
}

// リトライ可能かどうかを判定する関数
function shouldRetry(error: Error): boolean {
  return error instanceof NetworkError;
}

// 外部APIへのリクエストを行う非同期関数
async function fetchDataFromApi(): Promise<void> {
  // ダミーでランダムにエラーをスロー
  const randomFailure = Math.random() > 0.7; // 30%の確率で失敗
  if (randomFailure) {
    throw new NetworkError("ネットワーク接続に失敗しました");
  }
  console.log("データ取得成功");
}

// リトライロジックを組み込んだ処理
async function executeWithRetry(operation: () => Promise<void>, retries: number, delay: number): Promise<void> {
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      await operation(); // 実行
      return; // 成功したらリトライせずに終了
    } catch (error) {
      if (shouldRetry(error) && attempt < retries) {
        console.log(`リトライ中... (${attempt + 1}/${retries})`);
        await new Promise(resolve => setTimeout(resolve, delay)); // リトライ前に遅延
      } else {
        throw error; // リトライ不可、またはリトライ回数超過時にエラーを再スロー
      }
    }
  }
}

// 実際のリトライ処理の呼び出し
(async () => {
  try {
    await executeWithRetry(fetchDataFromApi, 3, 1000); // 最大3回リトライ、1秒間隔
    console.log("全てのリクエストが成功しました");
  } catch (error) {
    console.error("全てのリトライが失敗しました", error);
  }
})();

コードのポイント解説

  1. fetchDataFromApi関数:
  • この関数は、APIリクエストをシミュレートしており、30%の確率でNetworkErrorをスローします。
  • 成功するとデータが正常に取得された旨のメッセージを表示します。
  1. executeWithRetry関数:
  • operationとして渡された関数(この場合はfetchDataFromApi)を実行し、失敗した場合にリトライを試みます。
  • retriesパラメータでリトライ回数を指定し、delayでリトライの間隔(ミリ秒)を制御しています。
  1. shouldRetry関数:
  • 発生したエラーがNetworkErrorであればリトライ可能と判断します。これにより、特定のエラーだけがリトライ対象になります。
  1. リトライの間隔:
  • setTimeoutを利用して、リトライする前に指定した時間だけ待機します。これにより、すぐに再試行せず、一定の時間を置いてから再実行します。

リトライ処理の挙動

  • 初回のAPIリクエストが失敗した場合、リトライ処理が開始され、最大3回まで再試行します。
  • 各リトライの間には1秒の遅延があり、リトライに失敗した場合、最終的にエラーメッセージが表示されます。
  • 成功すればリトライ処理は終了し、成功メッセージが表示されます。

このように、特定のエラーに対してリトライを行い、一定の間隔で再試行することで、ネットワーク不良などの一時的な問題に対処しやすくなります。

リトライ回数や間隔の制御方法

リトライ処理を実装する際、リトライ回数やリトライ間隔を柔軟に制御することが重要です。これにより、システムの負荷を軽減しつつ、外部要因による一時的な障害に適切に対処できます。次に、リトライ回数や間隔の設定方法について解説します。

固定リトライ回数の設定

リトライ回数を固定に設定する場合、ユーザーが指定した回数までリトライを試みます。以下のコードでは、executeWithRetry関数にリトライ回数をパラメータとして渡し、その回数だけリトライする仕組みになっています。

async function executeWithRetry(operation: () => Promise<void>, retries: number, delay: number): Promise<void> {
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      await operation(); // 実行
      return; // 成功したら終了
    } catch (error) {
      if (shouldRetry(error) && attempt < retries) {
        console.log(`リトライ中... (${attempt + 1}/${retries})`);
        await new Promise(resolve => setTimeout(resolve, delay)); // 指定された遅延時間待機
      } else {
        throw error; // リトライ不可、または回数超過時にエラーをスロー
      }
    }
  }
}

ここでは、retriesパラメータを3に設定しているため、3回リトライが行われます。回数を増減することで、リトライの頻度を簡単に調整できます。

指数バックオフによるリトライ間隔の制御

固定のリトライ間隔では、リトライを何度も行った場合にリソースの無駄遣いになりがちです。そこで、リトライ間隔を指数関数的に増加させる「指数バックオフ」を使用することで、リトライ回数が増えるごとに待機時間も増加する仕組みが役立ちます。これは、特に外部APIやネットワーク障害の復旧を待つ場合に効果的です。

以下は、指数バックオフを利用したリトライの例です。

async function executeWithExponentialBackoff(
  operation: () => Promise<void>,
  retries: number,
  baseDelay: number
): Promise<void> {
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      await operation(); // 実行
      return; // 成功したら終了
    } catch (error) {
      if (shouldRetry(error) && attempt < retries) {
        const delay = baseDelay * Math.pow(2, attempt); // 指数関数的に遅延を増やす
        console.log(`リトライ中... (${attempt + 1}/${retries}) 次の試行までの待機時間: ${delay}ms`);
        await new Promise(resolve => setTimeout(resolve, delay)); // 指定された遅延時間待機
      } else {
        throw error; // リトライ不可、または回数超過時にエラーをスロー
      }
    }
  }
}

指数バックオフの動作

  • baseDelayを基本の待機時間として設定し、リトライが行われるたびにその時間を指数関数的に増やしていきます。例えば、baseDelayが1000ミリ秒(1秒)で、リトライ回数が増えるごとに2倍の遅延を加算します。
  • 1回目のリトライでは1秒
  • 2回目のリトライでは2秒
  • 3回目のリトライでは4秒
  • といった具合に待機時間が延長されていきます。

この方法により、リソースを効率的に使用しつつ、問題が解決するまでリトライを適切に遅延させることができます。

カスタマイズ可能なリトライ間隔

固定の遅延時間や指数バックオフ以外にも、リトライ間隔を柔軟に調整することが可能です。たとえば、各リトライごとに異なる遅延時間を設定したり、ネットワークの状況やAPIの応答時間に応じて動的に間隔を調整することも考えられます。

async function executeWithCustomRetryIntervals(
  operation: () => Promise<void>,
  retries: number,
  delays: number[]
): Promise<void> {
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      await operation(); // 実行
      return; // 成功したら終了
    } catch (error) {
      if (shouldRetry(error) && attempt < retries) {
        const delay = delays[attempt] || delays[delays.length - 1]; // 指定された間隔か、最後の間隔を使用
        console.log(`リトライ中... (${attempt + 1}/${retries}) 次の試行までの待機時間: ${delay}ms`);
        await new Promise(resolve => setTimeout(resolve, delay)); // 指定された遅延時間待機
      } else {
        throw error; // リトライ不可、または回数超過時にエラーをスロー
      }
    }
  }
}

この例では、delaysとしてそれぞれのリトライ間隔を配列で指定しています。たとえば、[1000, 2000, 5000]のように設定すれば、1回目のリトライは1秒、2回目は2秒、3回目は5秒の遅延となります。

最適なリトライ回数と間隔を選択するポイント

リトライ回数や間隔を決定する際の考慮事項として、次のポイントがあります。

  • システムの負荷: リトライを繰り返し行うことで、システムや外部リソースに負担をかける場合があります。リトライ回数を多く設定しすぎないことが重要です。
  • ネットワーク状況: ネットワークエラーの場合は、復旧に時間がかかることがあるため、指数バックオフやカスタム間隔を利用して遅延時間を調整するのが効果的です。
  • 外部APIの仕様: 使用しているAPIによっては、過度なリトライが制限されることもあります。そのため、APIのドキュメントを確認し、推奨されるリトライ戦略に従うことが大切です。

これらの調整によって、リトライ処理を効率的かつ効果的に制御でき、エラーが発生してもシステム全体に大きな影響を与えない設計が可能になります。

既存のライブラリやツールとの統合方法

TypeScriptでリトライ処理を実装する際、既存のライブラリやツールを活用することで、コードの簡略化や効率的な開発が可能です。これらのライブラリはリトライの実装をより洗練されたものにし、柔軟性と拡張性を向上させます。ここでは、代表的なライブラリとその統合方法について紹介します。

リトライ処理に便利なライブラリ

  1. axios-retry
    axiosはHTTPリクエストを簡単に行える人気のライブラリです。axios-retryはこのaxiosにリトライ機能を追加するライブラリで、失敗したリクエストを自動的にリトライする機能を提供します。
  2. retry
    retryは、リトライ処理の基本的なパターンを網羅したシンプルなライブラリです。リトライ回数や遅延時間、指数バックオフなど、さまざまなリトライ戦略を設定できます。

axios-retryの導入と設定

axios-retryは、外部APIへのリクエスト時に失敗した場合に自動的にリトライを行うライブラリです。axiosと一緒に使うことで、リトライ処理を簡単に実装できます。

まずはaxiosaxios-retryをインストールします。

npm install axios axios-retry

次に、リトライ設定を行います。

import axios from 'axios';
import axiosRetry from 'axios-retry';

// axiosにリトライ設定を追加
axiosRetry(axios, {
  retries: 3, // リトライ回数
  retryDelay: (retryCount) => {
    return retryCount * 1000; // リトライのたびに1秒待機
  },
  retryCondition: (error) => {
    return error.response?.status === 503; // HTTPステータス503の場合のみリトライ
  },
});

async function fetchData() {
  try {
    const response = await axios.get('https://api.example.com/data');
    console.log('データ取得成功:', response.data);
  } catch (error) {
    console.error('データ取得に失敗しました:', error);
  }
}

fetchData();

この例では、503 Service Unavailableエラーが発生した場合に最大3回までリトライする設定をしています。また、リトライごとに1秒間の遅延を設定しています。axios-retryを使用することで、リトライ処理をシンプルに組み込むことができます。

retryライブラリの導入と設定

retryは、カスタマイズ可能なリトライ処理を提供するシンプルなライブラリです。リトライの制御を細かく調整でき、特定のエラー条件やバックオフ戦略を柔軟に設定できます。

インストールは次のコマンドで行います。

npm install retry

次に、リトライ処理を設定します。

import * as retry from 'retry';

function operationThatMightFail(): Promise<void> {
  return new Promise((resolve, reject) => {
    const randomFailure = Math.random() > 0.7; // 30%の確率で失敗
    if (randomFailure) {
      reject(new Error('失敗しました'));
    } else {
      resolve();
    }
  });
}

function executeWithRetry() {
  const operation = retry.operation({ retries: 5, factor: 2, minTimeout: 1000 });

  operation.attempt(async (currentAttempt) => {
    try {
      await operationThatMightFail();
      console.log('成功しました');
    } catch (error) {
      if (operation.retry(error)) {
        console.log(`リトライ中... (${currentAttempt})`);
      } else {
        console.error('全てのリトライが失敗しました');
      }
    }
  });
}

executeWithRetry();

この例では、5回までリトライを行い、リトライのたびに待機時間を2倍に増加させる「指数バックオフ」を採用しています。retryライブラリを使用することで、シンプルに高度なリトライ処理を組み込むことができます。

その他のライブラリとの統合

  1. RxJS
    RxJSはリアクティブプログラミングをサポートするライブラリで、エラーハンドリングやリトライ処理に強力なツールを提供します。retryオペレーターを使用することで、非同期処理をリトライすることができます。
   import { of } from 'rxjs';
   import { retry, catchError } from 'rxjs/operators';

   const source = of('データ取得').pipe(
     retry(3), // 最大3回リトライ
     catchError((err) => {
       console.error('リトライに失敗しました:', err);
       throw err;
     })
   );

   source.subscribe({
     next: (value) => console.log(value),
     error: (error) => console.error('エラーが発生:', error),
   });
  1. p-retry
    p-retryはPromiseベースのリトライライブラリで、非同期関数に対してリトライを適用するのに便利です。リトライロジックをシンプルに実装でき、エラーハンドリングが簡単になります。
   import pRetry from 'p-retry';

   async function fetchData() {
     return await pRetry(async () => {
       // 外部APIからのデータ取得
       const response = await fetch('https://api.example.com/data');
       if (!response.ok) {
         throw new Error('データ取得失敗');
       }
       return await response.json();
     }, { retries: 3 });
   }

   fetchData()
     .then((data) => console.log('データ:', data))
     .catch((error) => console.error('全てのリトライが失敗しました:', error));

これらのライブラリを活用することで、TypeScriptのリトライ処理をより効率的に管理でき、エラーハンドリングの柔軟性が向上します。適切なツールを選んで統合することで、システムの信頼性と保守性を高めることが可能です。

リトライロジックのテスト方法

リトライ処理を実装した後、動作が期待通りであるかを確認するために、適切なテストを行うことが重要です。特に、エラーハンドリングやリトライ回数、遅延時間が正しく設定されているか、予期しない挙動が発生しないかを検証する必要があります。ここでは、リトライロジックのテスト方法について解説します。

モックとスタブを用いた単体テスト

リトライ処理のテストを行う際に、モックやスタブを使用することで、外部APIやネットワーク接続に依存せずに、テスト環境でリトライの動作を確認できます。これにより、テストを効率的に実行し、期待通りの結果を得ることができます。

以下に、jestを使用したモック関数を利用したテストの例を紹介します。

import { jest } from '@jest/globals';
import { executeWithRetry } from './retryLogic'; // リトライ処理を実装したモジュール

// リトライ対象のモック関数
const mockOperation = jest.fn();

// テストケース
describe('リトライロジックのテスト', () => {
  beforeEach(() => {
    mockOperation.mockClear(); // 各テスト実行前にモックをリセット
  });

  test('成功時にリトライを行わない', async () => {
    mockOperation.mockResolvedValueOnce('成功'); // 最初の実行で成功

    await executeWithRetry(mockOperation, 3, 1000); // 最大3回リトライ設定

    expect(mockOperation).toHaveBeenCalledTimes(1); // 1回のみ呼び出されるはず
  });

  test('失敗時に指定回数までリトライする', async () => {
    mockOperation
      .mockRejectedValueOnce(new Error('一時的な失敗')) // 1回目は失敗
      .mockRejectedValueOnce(new Error('一時的な失敗')) // 2回目も失敗
      .mockResolvedValueOnce('成功'); // 3回目で成功

    await executeWithRetry(mockOperation, 3, 1000);

    expect(mockOperation).toHaveBeenCalledTimes(3); // 合計3回呼び出される
  });

  test('リトライが全て失敗した場合にエラーをスローする', async () => {
    mockOperation.mockRejectedValue(new Error('失敗')); // 常に失敗する

    await expect(executeWithRetry(mockOperation, 3, 1000))
      .rejects.toThrow('失敗');

    expect(mockOperation).toHaveBeenCalledTimes(4); // 初回+3回のリトライで合計4回
  });
});

テストのポイント

  • モックの使用: 実際の外部依存やネットワーク接続をシミュレートするために、モック関数を利用します。これにより、特定の条件(成功・失敗)に基づくテストを実行できます。
  • 呼び出し回数の確認: モック関数が指定された回数だけリトライされたか、またはリトライせずに成功したかを確認します。これは、リトライ回数の検証に重要です。
  • エラー処理の確認: 全てのリトライが失敗した場合に、適切にエラーがスローされるかどうかをテストします。エラーの種類やメッセージも確認します。

遅延時間のテスト

リトライ時に遅延が正しく設定されているかをテストすることも重要です。jestのようなテスティングフレームワークでは、タイマーの挙動をモック化する機能があり、遅延時間が正しく計測されているかをテストできます。

jest.useFakeTimers(); // タイマーをモック化

test('リトライ間隔が正しく適用される', async () => {
  mockOperation.mockRejectedValue(new Error('失敗'));

  const retryPromise = executeWithRetry(mockOperation, 3, 1000);

  jest.advanceTimersByTime(1000); // 1秒進める
  expect(mockOperation).toHaveBeenCalledTimes(2); // 2回目のリトライが行われる

  jest.advanceTimersByTime(2000); // さらに2秒進める
  expect(mockOperation).toHaveBeenCalledTimes(3); // 3回目のリトライが行われる

  await expect(retryPromise).rejects.toThrow(); // 最終的には失敗
});

統合テスト

リトライ処理が実際にシステム全体でどのように動作するかを確認するために、統合テストも行うことが推奨されます。統合テストでは、実際のAPIやサービスと通信し、外部依存との連携が正しく行われているか、リトライ処理が適切に行われているかを確認します。以下は簡単な統合テストの例です。

import axios from 'axios';
import { executeWithRetry } from './retryLogic';

jest.mock('axios'); // axiosをモック化

test('APIへのリクエストが成功するまでリトライする', async () => {
  axios.get.mockRejectedValueOnce(new Error('503 Service Unavailable')) // 1回目のリクエストは失敗
    .mockRejectedValueOnce(new Error('503 Service Unavailable')) // 2回目も失敗
    .mockResolvedValueOnce({ data: '成功データ' }); // 3回目で成功

  const result = await executeWithRetry(() => axios.get('https://api.example.com/data'), 3, 1000);

  expect(result.data).toBe('成功データ');
  expect(axios.get).toHaveBeenCalledTimes(3); // 合計3回リクエスト
});

このテストでは、axiosをモック化し、APIリクエストの失敗と成功をシミュレートしています。モックを使用することで、実際のAPIを使わずにリトライ処理が正しく動作しているかを確認できます。

カバレッジとコード品質の向上

リトライ処理のテストを行う際、テストカバレッジの向上を意識して、エッジケース(例えば、エラーが発生しない場合、すべてのリトライが失敗する場合)を網羅的にテストすることが大切です。これにより、コードの信頼性と品質が向上し、本番環境での予期せぬエラーやバグを防ぐことができます。

テストの自動化ツールやCI/CDパイプラインを活用して、リトライロジックが他のコード変更に対しても正しく機能しているかを常に確認することが重要です。

カスタムエラーと標準エラーとの併用のポイント

TypeScriptでは、カスタムエラーと標準エラー(Errorクラス)を併用することで、より柔軟で詳細なエラーハンドリングが可能になります。特に、リトライ処理やエラーの分類において、カスタムエラーと標準エラーを適切に使い分けることが重要です。ここでは、その併用のポイントと実装の工夫について解説します。

カスタムエラーと標準エラーの使い分け

カスタムエラーは、アプリケーションの特定のロジックや要件に関連したエラー情報を追加したい場合に役立ちます。一方、標準エラーは一般的なエラーハンドリングや予期しない例外処理に使用できます。以下に、使い分けのポイントを紹介します。

  1. カスタムエラー
  • 特定のリトライ処理やエラーハンドリングのロジックをカプセル化したい場合。
  • 例えば、NetworkErrorTimeoutErrorのような特定の状況に対応したエラーを作成して、適切なリトライ処理やエラー通知を実装します。
  1. 標準エラー
  • 予期しないエラーや一般的なシステムエラーの処理に使用します。開発者が明示的に処理しない範囲で発生するエラー(例えば、TypeErrorReferenceErrorなど)をキャッチするのに適しています。

併用するシチュエーションの例

カスタムエラーと標準エラーを併用することで、エラーの特定やリトライロジックを柔軟に実装できます。例えば、次のようなシナリオでは、両者の併用が効果的です。

class NetworkError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'NetworkError';
  }
}

class TimeoutError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'TimeoutError';
  }
}

async function fetchData() {
  // 例外が発生する可能性のある処理
  throw new TimeoutError('リクエストがタイムアウトしました');
}

async function executeWithRetry() {
  try {
    await fetchData();
  } catch (error) {
    if (error instanceof NetworkError) {
      console.log('ネットワークエラーが発生しました。リトライを試みます。');
      // ネットワークエラーに対するリトライ処理
    } else if (error instanceof TimeoutError) {
      console.log('タイムアウトエラーが発生しました。リトライを試みます。');
      // タイムアウトエラーに対するリトライ処理
    } else {
      console.error('予期しないエラーが発生しました:', error);
      // 標準エラーへの対応(リトライせずに例外処理)
    }
  }
}

executeWithRetry();

この例では、NetworkErrorTimeoutErrorのようなカスタムエラーを使って、特定のエラーに対するリトライ処理を制御しています。一方で、catchブロック内で標準エラーを処理することで、予期しないエラーに対する一般的なエラーハンドリングも同時に行っています。

カスタムエラーの詳細情報の活用

カスタムエラーを使うことで、エラーの詳細情報を格納するプロパティを追加し、エラーハンドリングをより細かく制御できます。例えば、APIのレスポンスコードやリトライ回数、エラー発生時のタイムスタンプなどの情報を持たせることができます。

class ApiError extends Error {
  statusCode: number;
  retryCount: number;

  constructor(message: string, statusCode: number, retryCount: number) {
    super(message);
    this.name = 'ApiError';
    this.statusCode = statusCode;
    this.retryCount = retryCount;
  }
}

async function fetchData() {
  // エラーをスローし、詳細情報を追加
  throw new ApiError('APIエラーが発生しました', 503, 2);
}

async function executeWithRetry() {
  try {
    await fetchData();
  } catch (error) {
    if (error instanceof ApiError && error.statusCode === 503) {
      console.log(`ステータスコード ${error.statusCode} のエラー。リトライ回数: ${error.retryCount}`);
      // 503エラーに基づくリトライ処理
    } else {
      console.error('予期しないエラーが発生しました:', error);
    }
  }
}

executeWithRetry();

この例では、ApiErrorというカスタムエラークラスにstatusCoderetryCountといったプロパティを追加しています。これにより、エラー発生時に詳細な情報を取得し、エラーハンドリングをより柔軟に行えます。

カスタムエラーと標準エラーの併用時の注意点

  • 明確なエラーロジック: カスタムエラーを使用する場合、標準エラーと区別しやすくするため、エラー名やプロパティの設計を明確にしましょう。これにより、エラーハンドリングの際にエラーを識別しやすくなります。
  • 適切なエラーログの出力: カスタムエラーと標準エラーのどちらを処理する場合でも、適切なエラーログを出力することが重要です。特に、開発やデバッグの段階では、エラーの詳細情報が記録されることで問題の原因を特定しやすくなります。
  • 例外のキャッチ範囲を適切に設定: リトライ処理を行う際、カスタムエラーに対してだけリトライするように設定し、標準エラーや致命的なエラーは即座に処理を停止する設計にすることが望ましいです。これにより、無駄なリトライやエラーの隠蔽を防げます。

カスタムエラーと標準エラーを併用することで、より細かい制御を加えたエラーハンドリングが可能になり、システムの安定性と保守性を向上させることができます。

よくあるエラーハンドリングの落とし穴と対策

エラーハンドリングは、システムの安定性や信頼性を左右する重要な要素です。しかし、エラーハンドリングを適切に行わない場合、問題の発見が遅れたり、逆にシステム全体に悪影響を及ぼす可能性があります。ここでは、よくあるエラーハンドリングの落とし穴とその対策について解説します。

1. エラーを無視する

落とし穴:
エラーが発生しても、エラーメッセージを無視してしまうことはよくある誤りです。特に、catchブロックでエラーをキャッチした後に何もせず、エラーを握りつぶすコードは危険です。これにより、問題が隠れてしまい、原因不明のバグが発生する可能性があります。

対策:
すべてのエラーは、適切にログを残すか、必要に応じて処理を行うべきです。catchブロック内でエラーメッセージをログに出力したり、通知システムにエラーを送信することで、問題が発生した際に素早く対応できるようにします。

try {
  // エラーが発生する可能性のある処理
} catch (error) {
  console.error('エラー発生:', error); // エラーメッセージを適切に出力
}

2. 無限リトライによるシステム過負荷

落とし穴:
エラーハンドリングでリトライを行う際、リトライ回数に上限を設けないと、システムが無限ループに陥り、リソースを消耗させてしまう可能性があります。特に外部APIやネットワークリクエストの際、過剰なリトライがシステム全体に負荷をかけることになります。

対策:
必ずリトライ回数を制限し、適切な間隔を設けるようにします。また、リトライ時に指数バックオフ(リトライのたびに待機時間を増加させる)を採用することで、システム負荷を軽減できます。

async function executeWithRetry(operation: () => Promise<void>, retries: number, delay: number): Promise<void> {
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      await operation();
      return;
    } catch (error) {
      if (attempt < retries - 1) {
        await new Promise(resolve => setTimeout(resolve, delay));
      } else {
        throw error;
      }
    }
  }
}

3. エラーメッセージが曖昧すぎる

落とし穴:
エラーメッセージが不十分または曖昧な場合、問題の原因を特定するのに多くの時間を要します。「エラーが発生しました」というだけでは、デバッグが非常に困難です。

対策:
エラーメッセージには、何が原因でエラーが発生したのか、どの部分でエラーが発生したのかなど、具体的な情報を含めるようにします。また、エラーログにはスタックトレースも含めて記録することで、デバッグ時に役立つ情報を得られます。

catch (error) {
  console.error(`リクエスト失敗: ${error.message}`, error.stack); // 詳細なエラーメッセージとスタックトレース
}

4. エラーの特定が難しい

落とし穴:
エラー処理で、すべてのエラーを一括で処理してしまうと、特定のエラーと一般的なエラーの区別が難しくなり、正しい対応が取れない可能性があります。

対策:
エラーを細かく分類し、特定のエラーに対して適切なハンドリングを行うことが重要です。たとえば、ネットワークエラー、タイムアウトエラー、バリデーションエラーなど、それぞれ異なる対応を行うべきです。

try {
  // 処理
} catch (error) {
  if (error instanceof NetworkError) {
    console.log('ネットワークエラー: リトライを試みます。');
  } else if (error instanceof ValidationError) {
    console.log('バリデーションエラー: 入力を確認してください。');
  } else {
    console.error('予期しないエラー:', error);
  }
}

5. エラーハンドリングの共通化を怠る

落とし穴:
複数の場所で同じエラーハンドリングのロジックを実装すると、コードが重複しやすく、管理が難しくなります。さらに、将来的にエラーハンドリングを変更する際に、すべての場所で同じ修正が必要になります。

対策:
エラーハンドリングは、関数やユーティリティとして共通化することで、コードの重複を減らし、管理を容易にします。例えば、共通のリトライロジックを関数化することで、異なる処理で同じリトライロジックを再利用できます。

function handleError(error: Error) {
  if (error instanceof NetworkError) {
    console.log('ネットワークエラー: リトライ中...');
  } else {
    console.error('エラー:', error.message);
  }
}

// 各処理で共通のエラーハンドリングを呼び出し
try {
  // 処理
} catch (error) {
  handleError(error);
}

まとめ

エラーハンドリングの落とし穴を避けるためには、エラーを無視せずに適切な対策を講じ、リトライ処理やエラーメッセージを明確にすることが重要です。また、エラーの種類に応じた処理を行い、コードの再利用性を高めることで、システム全体の安定性とメンテナンス性を向上させることができます。

まとめ

本記事では、TypeScriptにおけるカスタムエラークラスを使ったリトライ処理の実装方法を中心に、リトライの必要性、カスタムエラーの作成方法、既存ライブラリとの統合、そしてテストの方法について詳しく解説しました。また、カスタムエラーと標準エラーの併用や、エラーハンドリングの際に陥りがちな落とし穴とその対策についても取り上げました。これらの知識を活用することで、エラー管理がより柔軟かつ効率的になり、信頼性の高いアプリケーションを構築することが可能になります。

コメント

コメントする

目次