TypeScriptでデコレーターを使って非同期関数にエラーハンドリングを追加する方法

TypeScriptで非同期関数を扱う際、エラーハンドリングは非常に重要です。非同期処理は、APIリクエストやファイル操作など、外部リソースを扱う際に頻繁に使用されますが、エラーが発生する可能性も高いため、適切なエラーハンドリングが不可欠です。本記事では、TypeScriptのデコレーター機能を使って、非同期関数に効率的かつ効果的なエラーハンドリングを追加する方法を解説します。これにより、コードの見通しを良くし、エラーが発生した際のデバッグも容易になります。

目次

TypeScriptのデコレーターとは

デコレーターは、TypeScriptでクラスやメソッドに追加の機能を付与するための強力な機能です。デコレーターは、クラス、メソッド、プロパティ、メソッドの引数などに適用でき、既存のコードに変更を加えることなく、簡単に新しい振る舞いを追加することができます。デコレーターのシンタックスは、@記号で始まり、関数として定義されます。

デコレーターを使うことで、ロジックの再利用が容易になり、コードのモジュール化やメンテナンスがしやすくなるのが利点です。たとえば、ログの記録やエラーハンドリングなど、複数の関数で共通する処理をデコレーターで統一することができます。

非同期関数のエラーハンドリングの重要性

非同期関数では、エラーが発生する可能性が常に伴います。APIリクエストの失敗、ファイルの読み書きエラー、ネットワークの不安定さなど、多くの要因がエラーの原因となり得ます。これらのエラーを適切に処理しないと、アプリケーションが予期せずクラッシュしたり、ユーザーに不適切なメッセージが表示されたりする可能性があります。

非同期エラーハンドリングの課題

非同期処理では、エラーは即座にスローされるわけではなく、後から発生することがあります。従来の同期的なtry-catchでは対応が難しい場合も多く、非同期関数特有のエラーハンドリングが必要です。特に、Promiseやasync/awaitを使う場面では、エラー処理の漏れが重大な問題を引き起こす可能性があります。

信頼性とユーザー体験の向上

エラーハンドリングを適切に実装することは、アプリケーションの信頼性を高め、ユーザーにより良い体験を提供するために非常に重要です。エラー発生時に、ユーザーに適切なメッセージを表示したり、再試行のオプションを提示したりすることで、スムーズなユーザー体験を維持することができます。

デコレーターでエラーハンドリングを追加する方法

TypeScriptのデコレーターを使用すると、非同期関数に簡単にエラーハンドリングを追加できます。これにより、エラーハンドリングのロジックを関数ごとに記述する必要がなく、コードの重複を避けつつ一貫性を持たせることができます。

基本的なデコレーターの構造

デコレーターを用いてエラーハンドリングを追加するためには、関数に適用するデコレーターを作成し、エラー発生時に適切な処理を行います。以下の例では、非同期関数にエラーハンドリングを組み込んだデコレーターを定義します。

function errorHandler(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = async function(...args: any[]) {
        try {
            return await originalMethod.apply(this, args);
        } catch (error) {
            console.error(`Error in ${propertyKey}:`, error);
            // エラーを投げる代わりに、ログを記録しエラー情報を返す
            return { success: false, message: error.message };
        }
    };

    return descriptor;
}

このerrorHandlerデコレーターは、非同期関数の中でエラーが発生した場合、catchブロックでエラーメッセージをログに出力し、関数が失敗したことを示すオブジェクトを返します。

デコレーターの適用例

次に、このデコレーターを非同期関数に適用する例を見てみましょう。

class ApiService {
    @errorHandler
    async fetchData(url: string) {
        const response = await fetch(url);
        if (!response.ok) {
            throw new Error('Network response was not ok');
        }
        return await response.json();
    }
}

const service = new ApiService();
service.fetchData('https://example.com/data')
    .then(result => console.log(result))
    .catch(error => console.error(error));

この例では、fetchData関数にデコレーターを適用し、エラーハンドリングが追加されています。これにより、fetchData関数が失敗した場合でも、エラーログが出力され、エラー情報が返されます。デコレーターを使うことで、エラーハンドリングのロジックを個々の関数内に記述する必要がなく、コードがシンプルかつ統一されたものになります。

非同期関数に対するtry-catchの限界

非同期関数のエラーハンドリングとして一般的な方法はtry-catchブロックを使用することですが、この手法には限界があります。特に、複数の非同期関数を扱う場合や再利用可能なエラーハンドリングを求める場合、try-catchでは複雑なロジックを管理するのが難しくなることがあります。

コードの冗長化と可読性の低下

各非同期関数でtry-catchを使ってエラーハンドリングを行う場合、同じようなエラーハンドリングコードを何度も書くことになります。これにより、コードが冗長になり、読みやすさや保守性が低下します。以下はその一例です。

async function fetchData() {
    try {
        const response = await fetch('https://api.example.com/data');
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('Error fetching data:', error);
        throw error;
    }
}

async function sendData() {
    try {
        const response = await fetch('https://api.example.com/send', { method: 'POST' });
        const result = await response.json();
        return result;
    } catch (error) {
        console.error('Error sending data:', error);
        throw error;
    }
}

この例では、2つの非同期関数でエラーハンドリングがそれぞれ実装されていますが、同じようなtry-catchブロックが繰り返されており、コードの冗長さが目立ちます。

大規模なプロジェクトでのスケーラビリティの欠如

小規模なプロジェクトであれば、try-catchブロックは問題ありません。しかし、大規模なプロジェクトでは、同様のエラーハンドリングを数十、数百の関数で適用する必要が出てくるため、個別にtry-catchを追加していくのは非効率的です。各関数にエラーハンドリングを入れるたびにコード量が増え、管理が煩雑になります。

複数の非同期操作における制御の難しさ

さらに、複数の非同期処理を並行して行う場合、try-catchだけではすべてのエラーを適切にキャッチすることが難しくなります。特にPromise.all()などを使うと、個々の非同期処理に対して個別にtry-catchを書くのは現実的ではありません。

async function processMultipleRequests() {
    try {
        const [data1, data2] = await Promise.all([fetchData1(), fetchData2()]);
        return { data1, data2 };
    } catch (error) {
        console.error('Error in one of the requests:', error);
        throw error;
    }
}

ここでも、どちらのリクエストでエラーが発生したかを具体的に把握するのが難しいですし、個々のエラーハンドリングを細かく制御することができません。

一貫したエラーハンドリングの難しさ

プロジェクト全体でエラーハンドリングを一貫して実施するためには、try-catchブロックを各関数に追加するだけでは不十分です。エラーの種類によって異なる処理を行いたい場合、関数ごとに異なるロジックを書く必要があり、それを管理するのは難しくなります。

デコレーターを使ったエラーハンドリングであれば、再利用可能な一貫したエラーハンドリングの実装が可能になり、コードのメンテナンス性や可読性も向上します。

エラーハンドリング用のデコレーターの実装例

デコレーターを使えば、エラーハンドリングのロジックを関数ごとに記述する必要がなくなり、より一貫性を持って管理できます。ここでは、非同期関数にエラーハンドリングを追加するためのデコレーターの具体的な実装例を紹介します。

エラーハンドリング用デコレーターの基本実装

以下は、非同期関数に対してエラーハンドリングを追加するデコレーターの基本実装です。このデコレーターでは、関数の実行中に発生するエラーをキャッチし、ログ出力やカスタムエラーメッセージを返すようにしています。

function errorHandler(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (...args: any[]) {
        try {
            return await originalMethod.apply(this, args);
        } catch (error) {
            console.error(`Error in method ${propertyKey}:`, error);
            // エラーメッセージをユーザー向けにカスタマイズして返す
            return { success: false, message: "An error occurred. Please try again later." };
        }
    };

    return descriptor;
}

このデコレーターは、非同期関数の呼び出しをラップしており、エラーが発生した場合にcatchブロックでログを出力し、ユーザーにわかりやすいメッセージを返すように設計されています。

デコレーターを適用する方法

このデコレーターを実際に非同期関数に適用してみましょう。次の例では、fetchDataというAPIコールを行う非同期関数にエラーハンドリングを追加しています。

class ApiService {
    @errorHandler
    async fetchData(url: string) {
        const response = await fetch(url);
        if (!response.ok) {
            throw new Error('Failed to fetch data from the server.');
        }
        return await response.json();
    }
}

このコードでは、fetchData関数に@errorHandlerデコレーターを適用することで、関数のエラーハンドリングが統一された形で追加されています。もしfetchData関数内でエラーが発生した場合、エラーメッセージがログに出力され、ユーザーには"An error occurred. Please try again later."というメッセージが返されます。

カスタムエラーハンドリングの追加

デコレーターの柔軟性を活かして、エラーハンドリングをカスタマイズすることも可能です。例えば、エラーの種類に応じて異なるメッセージを返すようにしたり、特定のエラーに対してリトライ処理を行うこともできます。

function customErrorHandler(retryCount: number = 0) {
    return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;

        descriptor.value = async function (...args: any[]) {
            let attempts = 0;
            while (attempts <= retryCount) {
                try {
                    return await originalMethod.apply(this, args);
                } catch (error) {
                    attempts++;
                    console.error(`Attempt ${attempts} - Error in method ${propertyKey}:`, error);
                    if (attempts > retryCount) {
                        return { success: false, message: "Failed after multiple attempts. Please try again later." };
                    }
                }
            }
        };

        return descriptor;
    };
}

このデコレーターでは、retryCountパラメーターを使用して、関数がエラーで失敗した場合にリトライ回数を指定することができます。指定した回数リトライした後にエラーが解決しなければ、カスタムメッセージを返します。

まとめ

このように、デコレーターを使用することで、非同期関数に対するエラーハンドリングを簡潔かつ再利用可能な形で実装することが可能です。プロジェクト全体で統一されたエラーハンドリングを行うための便利な手段として、デコレーターは非常に有効です。

デコレーターでエラーをログに記録する方法

非同期関数で発生したエラーをログに記録することは、エラーの原因を追跡し、問題を迅速に解決するために非常に重要です。デコレーターを活用すれば、関数ごとにエラーログを出力する処理を統一でき、コードの重複を防ぎながら簡単に実装できます。

エラーログを記録するデコレーターの実装

以下は、関数内で発生したエラーをキャッチし、ログに記録するためのデコレーターの実装です。このデコレーターは、エラーハンドリングに加えてエラーの詳細をコンソールや外部ログシステムに送信する機能を追加しています。

function logError(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (...args: any[]) {
        try {
            return await originalMethod.apply(this, args);
        } catch (error) {
            console.error(`Error in ${propertyKey}: ${error.message}`);
            // 外部のログシステムにエラーを送信する処理(例: Sentry, Datadogなど)
            logToExternalService(propertyKey, error);
            throw error; // 必要に応じてエラーを再スロー
        }
    };

    return descriptor;
}

function logToExternalService(methodName: string, error: any) {
    // ここにエラーを外部サービスに送信するロジックを記述
    console.log(`Logging error to external service: ${methodName}`, error);
}

このデコレーターでは、console.errorでエラーをログ出力すると同時に、logToExternalService関数を呼び出して外部のエラーログサービス(例えば、SentryやDatadogなど)にエラー情報を送信することができます。

デコレーターの適用例

次に、このlogErrorデコレーターを非同期関数に適用して、エラーが発生した場合にログが記録される仕組みを示します。

class ApiService {
    @logError
    async fetchData(url: string) {
        const response = await fetch(url);
        if (!response.ok) {
            throw new Error('Failed to fetch data from the server.');
        }
        return await response.json();
    }
}

const service = new ApiService();
service.fetchData('https://example.com/data')
    .then(result => console.log(result))
    .catch(error => console.error("Error caught in fetchData:", error));

この例では、fetchData関数にデコレーターが適用されているため、関数内でエラーが発生した場合には、そのエラーがコンソールに記録され、さらに外部ログサービスに送信されます。これにより、開発者はアプリケーションのエラーログを容易に追跡でき、問題発生時に迅速に対応できます。

外部ログサービスへの統合

エラーを記録する際に、実際の運用環境ではコンソールログだけでなく、SentryやDatadogのような外部ログサービスに統合することが一般的です。これにより、リアルタイムでエラーを監視したり、過去のエラー履歴を追跡したりすることが可能になります。

function logToExternalService(methodName: string, error: any) {
    // 例: SentryのAPIを使ってエラーを送信
    Sentry.captureException(error, {
        tags: { method: methodName },
    });
}

このように、デコレーターを使ってエラーログを外部サービスに送信する機能を組み込むことで、アプリケーションの可視性を高め、エラー管理を効率的に行うことができます。

まとめ

エラーログの記録は、エラー発生時に迅速に対処するために重要なステップです。デコレーターを使用することで、非同期関数で発生するエラーのログ出力を一貫して行うことができ、さらに外部サービスとの統合も容易に実現できます。

既存のコードにデコレーターを適用する手順

デコレーターを使用すると、既存の非同期関数にエラーハンドリング機能やエラーログの記録機能を簡単に追加できます。このセクションでは、既存コードにデコレーターを適用するための具体的な手順を説明します。

手順1: エラーハンドリングデコレーターの作成

まず、エラーハンドリングやログ記録を行うためのデコレーターを作成します。すでに紹介したエラーハンドリングデコレーターやエラーログ記録デコレーターをそのまま利用することができます。たとえば、次のようなlogErrorデコレーターを使います。

function logError(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (...args: any[]) {
        try {
            return await originalMethod.apply(this, args);
        } catch (error) {
            console.error(`Error in ${propertyKey}: ${error.message}`);
            logToExternalService(propertyKey, error); // 外部ログサービスへの送信
            throw error;
        }
    };

    return descriptor;
}

手順2: 既存の非同期関数にデコレーターを適用する

次に、既存の非同期関数にデコレーターを適用します。デコレーターを適用するには、関数の前に@デコレーター名を追加するだけです。

例えば、既存のAPI呼び出し関数にエラーハンドリングとログ記録機能を追加する例を見てみましょう。

既存のコード:

class ApiService {
    async fetchData(url: string) {
        const response = await fetch(url);
        if (!response.ok) {
            throw new Error('Failed to fetch data.');
        }
        return await response.json();
    }
}

デコレーター適用後:

class ApiService {
    @logError
    async fetchData(url: string) {
        const response = await fetch(url);
        if (!response.ok) {
            throw new Error('Failed to fetch data.');
        }
        return await response.json();
    }
}

このように、非同期関数の前に@logErrorを追加するだけで、エラーハンドリングとログ記録の機能が統一された形で適用されます。

手順3: 複数の関数にデコレーターを適用する

複数の非同期関数に同じデコレーターを適用する場合も、同様に@logErrorを関数の前に追加するだけで対応できます。例えば、複数のAPIリクエストを処理する関数にデコレーターを適用した例です。

class ApiService {
    @logError
    async fetchData(url: string) {
        const response = await fetch(url);
        if (!response.ok) {
            throw new Error('Failed to fetch data.');
        }
        return await response.json();
    }

    @logError
    async sendData(url: string, data: any) {
        const response = await fetch(url, { method: 'POST', body: JSON.stringify(data) });
        if (!response.ok) {
            throw new Error('Failed to send data.');
        }
        return await response.json();
    }
}

これにより、コード全体で統一されたエラーハンドリングとログの記録が可能になります。

手順4: 既存コードに対するテスト

既存のコードにデコレーターを適用した後は、非同期関数が正常に動作することを確認するためのテストを実行します。デコレーターを追加することで、既存の動作に影響が出ないか、エラーハンドリングが正しく機能しているかを確認しましょう。

(async () => {
    const service = new ApiService();
    try {
        await service.fetchData('https://example.com/data');
    } catch (error) {
        console.error('Test caught error:', error);
    }
})();

このテストにより、エラーハンドリングが期待通りに機能し、ログも正しく記録されているかを検証します。

まとめ

デコレーターを既存の非同期関数に適用する手順は非常にシンプルです。コードの可読性や保守性を高めるために、エラーハンドリングやログ記録をデコレーターとして実装し、既存の関数に適用することで、効率的に機能を追加できます。

エラーハンドリングとパフォーマンスのトレードオフ

エラーハンドリングを強化することは、アプリケーションの安定性やメンテナンス性を向上させるために重要ですが、これにはパフォーマンスへの影響も伴います。特に、デコレーターを使って非同期関数にエラーハンドリング機能を追加すると、処理のオーバーヘッドやリソース消費が増える可能性があります。このセクションでは、エラーハンドリングを実装する際のパフォーマンスへの影響について考察し、最適なバランスを取るための方法を紹介します。

エラーハンドリングによるオーバーヘッド

非同期関数にエラーハンドリングを追加することで、いくつかのオーバーヘッドが発生します。以下は、代表的なオーバーヘッドの例です。

  • try-catchブロックの追加処理:非同期関数でエラーをキャッチするためにtry-catchを追加すると、関数の実行に若干の遅延が生じることがあります。特にエラーが頻繁に発生する場合、その処理コストが蓄積する可能性があります。
  • ログの記録処理:エラー発生時にエラーログをコンソールや外部サービスに送信するための処理が、関数の実行速度に影響を与えることがあります。特に、外部のログサービスにアクセスする際は、通信コストやレイテンシの増加が考えられます。
  • デコレーターによる関数のラップ処理:デコレーターは関数をラップする形でエラーハンドリングを追加しますが、このラップ処理自体もパフォーマンスに影響を与える可能性があります。関数が頻繁に呼び出されると、この影響が無視できないものになることがあります。

パフォーマンスの影響を軽減する方法

エラーハンドリングを強化しつつ、パフォーマンスの低下を最小限に抑えるためには、以下の方法を検討することが有効です。

非同期関数の選択的エラーハンドリング

すべての非同期関数に同じエラーハンドリングを適用するのではなく、特にエラーの発生が多い、もしくはクリティカルな部分にのみデコレーターを適用することで、パフォーマンスの低下を防ぐことができます。たとえば、API呼び出しなど外部のリソースに依存する処理にはエラーハンドリングを追加し、内部処理には最低限のハンドリングのみを行うというアプローチです。

非同期ログ処理の導入

エラーログを外部サービスに送信する際、ログ処理を非同期で行うことがパフォーマンスの改善に役立ちます。これにより、関数の実行を止めることなくログの記録を行い、メイン処理のパフォーマンスに与える影響を最小限に抑えることができます。

function logErrorAsync(error: any) {
    setTimeout(() => {
        // 外部サービスに非同期でエラーを送信
        logToExternalService(error);
    }, 0);
}

このように、ログ処理を非同期で行うことで、メインの処理フローに影響を与えずにエラーハンドリングを行うことが可能です。

デコレーターのカスタマイズによる最適化

デコレーター自体をパフォーマンスに配慮した形でカスタマイズすることも一つの方法です。例えば、特定の条件下でのみデコレーターを適用する、あるいはデコレーター内部で軽量なエラーハンドリングを実施するなど、細かいチューニングが可能です。

function logError(condition: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = async function (...args: any[]) {
            if (condition) {
                try {
                    return await originalMethod.apply(this, args);
                } catch (error) {
                    console.error(`Error in ${propertyKey}: ${error.message}`);
                    logToExternalService(propertyKey, error);
                    throw error;
                }
            } else {
                return await originalMethod.apply(this, args);
            }
        };
        return descriptor;
    };
}

このように、条件に応じてエラーハンドリングの処理を選択することで、パフォーマンスへの影響を最小限に抑えることができます。

エラーハンドリングとパフォーマンスのバランス

エラーハンドリングとパフォーマンスのバランスを取るためには、以下の点を意識することが重要です。

  1. 重要度の高い処理に集中してエラーハンドリングを適用する: すべての関数にエラーハンドリングを追加するのではなく、エラー発生が致命的となる部分にのみ適用する。
  2. ログ処理を非同期化する: エラーログの記録を非同期で行い、パフォーマンスへの影響を抑える。
  3. デコレーターの条件付けや最適化を行う: 関数の種類や状況に応じてデコレーターをカスタマイズし、無駄な処理を省く。

まとめ

エラーハンドリングを強化するとパフォーマンスへの影響は避けられませんが、適切な設計や非同期処理を取り入れることで、その影響を最小限に抑えることが可能です。エラーハンドリングとパフォーマンスのトレードオフを意識しながら、アプリケーションの安定性と効率性のバランスを取りましょう。

実践的な応用例:APIコールでのエラーハンドリング

非同期関数でのエラーハンドリングは、特にAPIコールでの実装が重要です。APIコールは外部のサーバーとの通信が必要であり、ネットワークエラーやサーバーエラー、認証の失敗など、さまざまなエラーが発生する可能性があります。デコレーターを使ってエラーハンドリングを効率化することで、エラー発生時にもアプリケーションの動作を安定させ、ユーザーに適切なフィードバックを提供できます。

ここでは、APIコールにおけるエラーハンドリングの実践的な応用例を紹介します。

基本的なAPIコールのデコレーター

以下は、APIリクエストにエラーハンドリングとログ記録機能を組み込んだデコレーターの実装例です。このデコレーターは、APIコール中に発生したエラーをキャッチし、エラーメッセージをユーザーに返すとともに、エラーログを記録します。

function apiErrorHandler(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (...args: any[]) {
        try {
            const result = await originalMethod.apply(this, args);
            return result;
        } catch (error) {
            console.error(`API call failed in ${propertyKey}: ${error.message}`);
            logToExternalService(propertyKey, error);  // 外部ログサービスにエラーを送信
            return { success: false, message: 'API request failed. Please try again later.' };
        }
    };

    return descriptor;
}

このデコレーターは、APIコールが失敗した際にエラーをログに残し、ユーザーには「APIリクエストに失敗しました。後でもう一度試してください。」というメッセージを表示します。

APIコールにデコレーターを適用する

次に、上記のapiErrorHandlerデコレーターをAPIコールを行う関数に適用してみましょう。以下は、APIからデータを取得する関数にデコレーターを適用した例です。

class ApiService {
    @apiErrorHandler
    async getData(endpoint: string) {
        const response = await fetch(endpoint);
        if (!response.ok) {
            throw new Error(`Failed to fetch data from ${endpoint}`);
        }
        return await response.json();
    }
}

このように、@apiErrorHandlerデコレーターを追加するだけで、APIリクエスト時のエラーハンドリングとログ記録が一括で適用されます。

APIコール時のリトライ処理

APIコールでは、一時的なネットワーク障害などによってリクエストが失敗することがあります。このようなケースでは、エラー発生時にリクエストを数回リトライする機能をデコレーターに追加することが有効です。次の例では、指定された回数だけリトライを行うデコレーターを実装しています。

function apiErrorHandlerWithRetry(retryCount: number = 3) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;

        descriptor.value = async function (...args: any[]) {
            let attempts = 0;
            while (attempts < retryCount) {
                try {
                    const result = await originalMethod.apply(this, args);
                    return result;
                } catch (error) {
                    attempts++;
                    console.warn(`Attempt ${attempts} failed: ${error.message}`);
                    if (attempts >= retryCount) {
                        console.error(`API call failed after ${attempts} attempts in ${propertyKey}`);
                        logToExternalService(propertyKey, error);  // エラーを外部サービスに記録
                        return { success: false, message: 'API request failed after multiple attempts.' };
                    }
                }
            }
        };

        return descriptor;
    };
}

このデコレーターは、APIコールが失敗した場合にリクエストをリトライします。リトライ回数を超えた場合には、エラーログを記録し、ユーザーに適切なメッセージを返します。

リトライ処理付きデコレーターの適用例

このリトライ処理付きデコレーターを実際のAPIコールに適用してみましょう。以下のコードは、指定されたAPIエンドポイントに対してリトライ処理を行うgetData関数の例です。

class ApiService {
    @apiErrorHandlerWithRetry(3)
    async getData(endpoint: string) {
        const response = await fetch(endpoint);
        if (!response.ok) {
            throw new Error(`Failed to fetch data from ${endpoint}`);
        }
        return await response.json();
    }
}

このように、@apiErrorHandlerWithRetry(3)デコレーターを使用することで、APIコールが失敗した場合に最大3回までリトライを行い、それでも失敗した場合にエラーログを記録し、ユーザーにメッセージを返します。

実践でのデコレーター活用のメリット

APIコールにデコレーターを適用することで、以下のようなメリットがあります。

  • 一貫したエラーハンドリング: すべてのAPIリクエストで統一されたエラーハンドリングを実装でき、エラー時の動作が一貫します。
  • コードの簡潔化: 各API関数に個別でエラーハンドリングを記述する必要がなくなり、コードの冗長性が削減されます。
  • 再利用性の向上: リトライ処理やログ記録など、共通の処理をデコレーターに集約することで、機能の再利用性が高まります。

まとめ

APIコールにおけるデコレーターの活用は、エラーハンドリングやリトライ処理、ログ記録などの複雑なロジックをシンプルにし、コード全体の管理を効率化します。特に、エラーが発生しやすいAPIリクエストでは、デコレーターを利用することで、信頼性とパフォーマンスのバランスを取りつつ、より洗練された非同期処理を実現できます。

デコレーターを使った非同期関数のテスト方法

デコレーターを利用して非同期関数にエラーハンドリングやリトライ処理を追加した場合、適切に機能しているかを確認するためのテストが重要です。テストでは、関数がエラーを正しくキャッチするか、リトライ処理が適切に行われているかなどを検証します。ここでは、デコレーターを適用した非同期関数のテスト方法を解説します。

基本的なテスト戦略

デコレーター付きの非同期関数をテストする際には、次のようなポイントに着目します。

  1. エラーハンドリングの動作確認: エラーが発生した場合に、エラーが正しくキャッチされ、ログが記録されるかどうかを確認します。
  2. リトライ処理の検証: リトライ機能がある場合、指定された回数リトライされるか、リトライ回数を超えた後にエラーが返されるかを確認します。
  3. 外部サービスへのログ送信: エラーが発生した際に、外部ログサービスへの送信が正しく行われるかを確認します。

テスト環境の準備

デコレーター付きの非同期関数をテストするには、テストフレームワークとしてJestなどを使用します。まず、テストの準備として、モックを使用して外部サービスの呼び出しやAPIコールを模擬します。

import { jest } from '@jest/globals';

// 外部サービスのログ送信関数をモック
jest.mock('./logService', () => ({
    logToExternalService: jest.fn(),
}));

import { logToExternalService } from './logService';
import { ApiService } from './apiService';  // デコレーター適用済みのクラス

describe('ApiService Tests', () => {
    let service: ApiService;

    beforeEach(() => {
        service = new ApiService();
    });

    afterEach(() => {
        jest.clearAllMocks();  // 各テスト後にモックをクリア
    });

    // テストケースを追加していきます
});

エラーハンドリングのテスト

非同期関数がエラーを正しくキャッチし、エラーメッセージが期待通りに返されるかをテストします。この例では、fetchData関数がAPIコールに失敗したときに、エラーがキャッチされ、適切なメッセージが返されるかを確認します。

it('should return a custom error message on API failure', async () => {
    global.fetch = jest.fn(() => Promise.reject(new Error('Network Error')));  // fetchをモック

    const result = await service.getData('https://example.com/data');

    expect(result).toEqual({ success: false, message: 'API request failed. Please try again later.' });
    expect(logToExternalService).toHaveBeenCalled();  // ログ送信が行われたか確認
});

このテストでは、fetchData関数がfetchのエラーレスポンスを処理し、デコレーターがエラーハンドリングを適用していることを確認します。また、エラーログが外部サービスに送信されるかもチェックしています。

リトライ処理のテスト

リトライ機能をテストするには、非同期関数が指定された回数だけリトライを行い、その後にエラーが返されるかを検証します。

it('should retry the API call 3 times on failure', async () => {
    global.fetch = jest.fn(() => Promise.reject(new Error('Network Error')));  // fetchをモック

    const result = await service.getData('https://example.com/data');

    expect(global.fetch).toHaveBeenCalledTimes(3);  // 3回リトライされたか確認
    expect(result).toEqual({ success: false, message: 'API request failed after multiple attempts.' });
});

このテストでは、APIコールが3回リトライされた後にエラーメッセージが返されるかどうかを確認しています。

ログ記録のテスト

エラーが発生した際に、外部ログサービスに正しくエラー情報が送信されているかを確認します。このテストでは、モックを使用してログ送信関数が適切に呼び出されているかを検証します。

it('should log error to external service on failure', async () => {
    global.fetch = jest.fn(() => Promise.reject(new Error('Network Error')));  // fetchをモック

    await service.getData('https://example.com/data');

    expect(logToExternalService).toHaveBeenCalledWith('getData', new Error('Network Error'));
});

このテストでは、エラー発生時にlogToExternalService関数が正しく呼び出されているか、引数として正しいエラー情報が渡されているかを確認します。

エラーフリーの正常動作テスト

エラーハンドリングが適切に動作しているかを確認するだけでなく、エラーが発生しない場合に関数が正常に動作しているかもテストします。

it('should return data on successful API call', async () => {
    global.fetch = jest.fn(() =>
        Promise.resolve({
            ok: true,
            json: () => Promise.resolve({ data: 'sample data' }),
        })
    );  // 成功時のfetchをモック

    const result = await service.getData('https://example.com/data');

    expect(result).toEqual({ data: 'sample data' });
    expect(logToExternalService).not.toHaveBeenCalled();  // ログ送信は呼び出されない
});

このテストでは、APIコールが成功した場合に正しいデータが返され、エラーログ送信が不要であることを確認しています。

まとめ

デコレーターを使った非同期関数のテストでは、エラーハンドリングやリトライ処理、ログ送信が正しく機能しているかを確認することが重要です。Jestのようなテストフレームワークを使えば、モックを用いて外部依存の部分を模擬しながら、確実に各機能が期待通りに動作しているかを検証できます。これにより、デコレーターを利用したコードの品質を担保し、エラーに強いアプリケーションを構築できます。

まとめ

本記事では、TypeScriptのデコレーターを使って非同期関数にエラーハンドリングを追加する方法について詳しく解説しました。デコレーターを活用することで、非同期関数に対するエラーハンドリングの処理を一貫させ、コードの冗長性を減らし、保守性を向上させることが可能です。特に、APIコールでのリトライ処理やエラーログの記録といった実践的なシナリオにも応用できます。パフォーマンスと安定性を考慮しながら、エラーに強いアプリケーションを構築しましょう。

コメント

コメントする

目次