JavaScriptのPromiseを使ったエラーハンドリング徹底解説

JavaScriptにおいて、非同期処理は避けて通れない重要な概念です。非同期処理を効率的に管理するための方法の一つがPromiseです。Promiseは、非同期処理の結果を表すオブジェクトで、成功時の処理と失敗時の処理を簡潔に記述できます。しかし、非同期処理におけるエラーハンドリングは、複雑で理解が難しいことがあります。適切にエラーハンドリングを行わないと、予期せぬ動作やバグが発生する可能性があります。本記事では、JavaScriptのPromiseを使ったエラーハンドリングについて、基本から応用まで詳しく解説します。これにより、非同期処理におけるエラーを効果的に管理し、より堅牢なコードを作成するための知識を提供します。

目次

Promiseとは何か

PromiseはJavaScriptにおける非同期処理を管理するためのオブジェクトです。Promiseは、非同期操作が成功した場合と失敗した場合の処理をそれぞれ定義できるため、コールバック地獄(callback hell)を避けることができます。

Promiseの基本概念

Promiseは、非同期操作の結果を表し、以下の3つの状態を持ちます:

  • Pending(保留中): 初期状態。操作が完了していない状態。
  • Fulfilled(成功): 操作が正常に完了した状態。
  • Rejected(失敗): 操作が失敗した状態。

Promiseの利点

Promiseを使用することで、以下のような利点があります:

  • コードの可読性が向上する。
  • エラーハンドリングが統一される。
  • 複数の非同期操作をシンプルに管理できる。

基本的な使い方

Promiseは次のように使用します:

let promise = new Promise((resolve, reject) => {
    // 非同期処理
    if (成功) {
        resolve("成功の結果");
    } else {
        reject("エラーメッセージ");
    }
});

promise.then(result => {
    console.log(result); // "成功の結果"
}).catch(error => {
    console.log(error); // "エラーメッセージ"
});

この例では、Promiseが成功した場合はresolveが呼ばれ、失敗した場合はrejectが呼ばれます。thenメソッドで成功時の処理を、catchメソッドで失敗時の処理を定義しています。

Promiseの基本構文

Promiseを理解するために、基本的な構文を見ていきましょう。Promiseは、主にコンストラクタ関数とthencatch、およびfinallyメソッドを使用します。

Promiseの作成

Promiseは、次のようにして新しく作成されます:

let promise = new Promise((resolve, reject) => {
    // 非同期処理をここに書く
    let success = true; // 成功するかどうかの判定

    if (success) {
        resolve("処理が成功しました");
    } else {
        reject("処理が失敗しました");
    }
});

この例では、resolve関数が呼ばれるとPromiseは成功し、reject関数が呼ばれるとPromiseは失敗します。

thenメソッド

Promiseが成功した場合の処理は、thenメソッドを使用して定義します:

promise.then(result => {
    console.log(result); // "処理が成功しました"
});

thenメソッドは、Promiseが成功したときに実行されるコールバック関数を引数に取ります。このコールバック関数は、resolve関数から渡された値を受け取ります。

catchメソッド

Promiseが失敗した場合の処理は、catchメソッドを使用して定義します:

promise.catch(error => {
    console.log(error); // "処理が失敗しました"
});

catchメソッドは、Promiseが失敗したときに実行されるコールバック関数を引数に取ります。このコールバック関数は、reject関数から渡されたエラーメッセージを受け取ります。

finallyメソッド

Promiseが成功または失敗した後に必ず実行される処理は、finallyメソッドを使用して定義します:

promise.finally(() => {
    console.log("処理が完了しました");
});

finallyメソッドは、Promiseの状態に関係なく必ず実行されるコールバック関数を引数に取ります。これにより、リソースの解放や後処理を簡潔に記述することができます。

これらの基本構文を理解することで、Promiseを使った非同期処理の流れを効果的に管理できるようになります。

Promiseのエラーハンドリング

Promiseを使用した非同期処理では、エラーハンドリングが非常に重要です。適切にエラーハンドリングを行うことで、予期しない動作やバグを防ぐことができます。ここでは、Promiseのエラーハンドリングの基本的な方法を説明します。

try-catch構文によるエラーハンドリング

通常の同期処理では、エラーハンドリングにtry-catch構文を使用します。しかし、Promiseを使った非同期処理では、try-catch構文は使用できません。その代わりに、Promiseのメソッドを活用します。

Promise.catchメソッド

Promiseでエラーが発生した場合、catchメソッドを使用してエラーをキャッチします。catchメソッドは、Promiseがrejectされたときに実行されるコールバック関数を引数に取ります。

let promise = new Promise((resolve, reject) => {
    let success = false; // 成功するかどうかの判定

    if (success) {
        resolve("処理が成功しました");
    } else {
        reject("処理が失敗しました");
    }
});

promise.then(result => {
    console.log(result);
}).catch(error => {
    console.error(error); // "処理が失敗しました"
});

この例では、successfalseの場合にrejectが呼ばれ、catchメソッドがエラーメッセージをキャッチしてログに出力します。

エラーハンドリングのチェーン

複数の非同期操作が連続して行われる場合、エラーハンドリングをPromiseチェーンの最後にまとめて記述することができます。

let promise = new Promise((resolve, reject) => {
    // 非同期処理1
    resolve("最初の処理が成功しました");
});

promise.then(result => {
    console.log(result); // "最初の処理が成功しました"
    return new Promise((resolve, reject) => {
        // 非同期処理2
        reject("次の処理が失敗しました");
    });
}).then(result => {
    console.log(result);
}).catch(error => {
    console.error(error); // "次の処理が失敗しました"
});

この例では、最初のPromiseが成功した後、次のPromiseが失敗すると、catchメソッドでエラーがキャッチされます。このように、Promiseチェーンの最後にcatchメソッドを配置することで、一括してエラーハンドリングを行うことができます。

finallyメソッドによる後処理

Promiseが成功または失敗した後に必ず実行される処理は、finallyメソッドを使用して定義します。これは、リソースの解放や後処理を行う際に便利です。

let promise = new Promise((resolve, reject) => {
    resolve("処理が成功しました");
});

promise.then(result => {
    console.log(result);
}).catch(error => {
    console.error(error);
}).finally(() => {
    console.log("処理が完了しました");
});

この例では、finallyメソッドがPromiseの状態に関係なく実行され、処理の完了をログに出力します。

これらのエラーハンドリング方法を適切に活用することで、Promiseを使った非同期処理の信頼性と可読性を向上させることができます。

非同期処理におけるエラーハンドリング

非同期処理におけるエラーハンドリングは、同期処理と異なり、特殊な方法が必要です。非同期処理が失敗した場合でも、アプリケーション全体が影響を受けないようにするためには、適切なエラーハンドリングが不可欠です。

非同期処理の重要性

非同期処理は、ウェブアプリケーションやサーバーサイドプログラムにおいて、以下のような場面で頻繁に使用されます:

  • データベースへの問い合わせ
  • APIへのリクエスト
  • ファイルの読み書き

これらの操作は時間がかかるため、非同期で処理することで、他の処理をブロックせずに進めることができます。

非同期処理のエラーハンドリング方法

非同期処理のエラーハンドリングには、主に以下の方法があります:

コールバック関数を用いたエラーハンドリング

コールバック関数は、非同期処理の結果を受け取るために使用されます。エラーハンドリングもコールバック関数内で行います。

function asyncOperation(callback) {
    setTimeout(() => {
        let error = true;
        if (error) {
            callback("エラーが発生しました", null);
        } else {
            callback(null, "成功しました");
        }
    }, 1000);
}

asyncOperation((error, result) => {
    if (error) {
        console.error(error);
    } else {
        console.log(result);
    }
});

この例では、エラーが発生した場合にコールバック関数の第一引数にエラーメッセージが渡され、エラーハンドリングを行います。

Promiseを用いたエラーハンドリング

Promiseは、非同期処理のエラーハンドリングをよりシンプルにするために使用されます。

let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        let error = true;
        if (error) {
            reject("エラーが発生しました");
        } else {
            resolve("成功しました");
        }
    }, 1000);
});

promise.then(result => {
    console.log(result);
}).catch(error => {
    console.error(error);
});

この例では、Promiseがエラーを検出した場合にrejectが呼ばれ、catchメソッドでエラーメッセージをキャッチして処理します。

async/awaitを用いたエラーハンドリング

async/await構文は、Promiseをさらに使いやすくするための構文で、非同期処理を同期処理のように書くことができます。エラーハンドリングはtry-catch構文を用いて行います。

async function asyncOperation() {
    let error = true;
    if (error) {
        throw new Error("エラーが発生しました");
    }
    return "成功しました";
}

(async () => {
    try {
        let result = await asyncOperation();
        console.log(result);
    } catch (error) {
        console.error(error.message);
    }
})();

この例では、awaitで非同期処理の完了を待ち、エラーが発生した場合にcatchブロックでエラーメッセージを処理します。

これらの方法を理解し、適切にエラーハンドリングを実装することで、非同期処理における信頼性と可読性が向上します。次に、Promiseチェーンとエラーハンドリングについて詳しく見ていきます。

Promiseチェーンとエラーハンドリング

Promiseチェーンは、複数の非同期処理を順番に実行するための強力な方法です。Promiseチェーンを使用することで、非同期処理を直線的に書くことができ、可読性が向上します。しかし、チェーン内で発生するエラーを適切にハンドリングすることが重要です。

Promiseチェーンの基本

Promiseチェーンは、thenメソッドを連続して呼び出すことで構築されます。各thenメソッドは前のPromiseが解決(成功)または拒否(失敗)された後に実行されるコールバック関数を受け取ります。

let promise = new Promise((resolve, reject) => {
    resolve("最初の処理が成功しました");
});

promise.then(result => {
    console.log(result); // "最初の処理が成功しました"
    return "次の処理も成功しました";
}).then(result => {
    console.log(result); // "次の処理も成功しました"
}).catch(error => {
    console.error(error);
});

この例では、最初のPromiseが成功すると、次のthenメソッドが実行されます。最後に、チェーン全体のエラーハンドリングのためにcatchメソッドが使用されています。

チェーン内のエラーハンドリング

Promiseチェーン内でエラーが発生した場合、そのエラーは次に出現するcatchメソッドによってキャッチされます。これは、チェーン内のどのPromiseでエラーが発生しても、その後の処理を中断してエラーハンドリングに移行することを意味します。

let promise = new Promise((resolve, reject) => {
    resolve("最初の処理が成功しました");
});

promise.then(result => {
    console.log(result);
    return new Promise((resolve, reject) => {
        reject("次の処理でエラーが発生しました");
    });
}).then(result => {
    console.log(result); // この行は実行されません
}).catch(error => {
    console.error(error); // "次の処理でエラーが発生しました"
});

この例では、2番目のPromiseが拒否されるため、2番目のthenメソッドは実行されず、直接catchメソッドが実行されます。

個別のエラーハンドリング

Promiseチェーン内の各段階で個別にエラーハンドリングを行うこともできます。この方法は、特定の非同期処理に対して異なるエラーハンドリングを行いたい場合に有用です。

let promise = new Promise((resolve, reject) => {
    resolve("最初の処理が成功しました");
});

promise.then(result => {
    console.log(result);
    return new Promise((resolve, reject) => {
        reject("次の処理でエラーが発生しました");
    });
}).catch(error => {
    console.error("個別のエラーハンドリング:", error);
    return "エラー後のデフォルト値";
}).then(result => {
    console.log(result); // "エラー後のデフォルト値"
}).catch(error => {
    console.error("最終的なエラーハンドリング:", error);
});

この例では、2番目のPromiseでエラーが発生した場合、個別のcatchメソッドでエラーを処理し、エラー後のデフォルト値を返します。これにより、チェーン全体が継続されます。

まとめ

Promiseチェーンを使用することで、複数の非同期処理を順番に実行し、エラーが発生した場合は適切にハンドリングすることができます。各段階で個別のエラーハンドリングを行うか、チェーン全体でまとめてエラーハンドリングを行うかは、具体的な要件に応じて選択します。次に、async/awaitを使ったエラーハンドリングの方法を見ていきます。

async/awaitとエラーハンドリング

async/await構文は、Promiseをより直感的に扱うための構文です。これにより、非同期処理を同期処理のように記述でき、コードの可読性が大幅に向上します。ここでは、async/awaitを使ったエラーハンドリングの方法について説明します。

async/awaitの基本

asyncキーワードは関数を非同期関数にし、その関数はPromiseを返します。awaitキーワードは、Promiseが解決されるまで待ちます。

async function fetchData() {
    let response = await fetch('https://api.example.com/data');
    let data = await response.json();
    return data;
}

fetchData().then(data => {
    console.log(data);
}).catch(error => {
    console.error(error);
});

この例では、fetchData関数内でawaitを使用して非同期処理を待ち、結果を取得しています。

try-catch構文によるエラーハンドリング

async/await構文を使った非同期処理でエラーハンドリングを行うには、try-catch構文を使用します。これにより、同期処理のようにエラーハンドリングを記述できます。

async function fetchData() {
    try {
        let response = await fetch('https://api.example.com/data');
        if (!response.ok) {
            throw new Error('ネットワーク応答が正しくありません');
        }
        let data = await response.json();
        return data;
    } catch (error) {
        console.error('データの取得中にエラーが発生しました:', error);
        throw error; // 必要に応じて再スロー
    }
}

(async () => {
    try {
        let data = await fetchData();
        console.log(data);
    } catch (error) {
        console.error('fetchData関数がエラーを投げました:', error);
    }
})();

この例では、fetchData関数内で発生したエラーをtry-catch構文でキャッチし、エラーメッセージをログに出力しています。関数を呼び出す側でも、再度try-catch構文を使用してエラーをキャッチしています。

ネストされた非同期処理のエラーハンドリング

ネストされた非同期処理でも、try-catch構文を使ってエラーハンドリングを行えます。これにより、複数の非同期処理が関係する場合でもエラーハンドリングが簡潔になります。

async function fetchUserData(userId) {
    try {
        let userResponse = await fetch(`https://api.example.com/users/${userId}`);
        if (!userResponse.ok) {
            throw new Error('ユーザーデータの取得に失敗しました');
        }
        let userData = await userResponse.json();

        let postsResponse = await fetch(`https://api.example.com/users/${userId}/posts`);
        if (!postsResponse.ok) {
            throw new Error('ユーザーの投稿の取得に失敗しました');
        }
        let userPosts = await postsResponse.json();

        return { userData, userPosts };
    } catch (error) {
        console.error('ユーザーデータまたは投稿データの取得中にエラーが発生しました:', error);
        throw error;
    }
}

(async () => {
    try {
        let { userData, userPosts } = await fetchUserData(1);
        console.log(userData, userPosts);
    } catch (error) {
        console.error('fetchUserData関数がエラーを投げました:', error);
    }
})();

この例では、ユーザーデータと投稿データを順番に取得し、それぞれの非同期処理に対して個別にエラーハンドリングを行っています。これにより、どの部分でエラーが発生したかを明確にし、適切に対応できます。

まとめ

async/await構文を使用することで、非同期処理を直感的かつシンプルに記述でき、エラーハンドリングも同期処理と同様にtry-catch構文を使用して簡潔に行えます。次に、エラーハンドリングのベストプラクティスについて説明します。

エラーハンドリングのベストプラクティス

非同期処理におけるエラーハンドリングは、適切に設計し実装することで、コードの信頼性と可読性を大幅に向上させることができます。ここでは、Promiseとasync/awaitを使用したエラーハンドリングのベストプラクティスを紹介します。

1. 一貫したエラーハンドリング戦略を採用する

プロジェクト全体で一貫したエラーハンドリング戦略を採用することが重要です。Promiseチェーンやasync/awaitを使用する際、同じパターンでエラーを処理することで、コードの理解とメンテナンスが容易になります。

async function fetchData(url) {
    try {
        let response = await fetch(url);
        if (!response.ok) {
            throw new Error('ネットワーク応答が正しくありません');
        }
        return await response.json();
    } catch (error) {
        console.error('データの取得中にエラーが発生しました:', error);
        throw error; // 必要に応じて再スロー
    }
}

2. エラーのロギングと通知

エラーが発生した場合、適切にログを記録し、必要に応じて通知を行うことで、迅速に対応できるようにします。エラーログには、エラーの詳細情報(メッセージ、スタックトレースなど)を含めることが重要です。

function logError(error) {
    console.error('エラーが発生しました:', error.message);
    // ここにエラーログを保存するコードや通知を送るコードを追加
}

async function fetchData(url) {
    try {
        let response = await fetch(url);
        if (!response.ok) {
            throw new Error('ネットワーク応答が正しくありません');
        }
        return await response.json();
    } catch (error) {
        logError(error);
        throw error; // 必要に応じて再スロー
    }
}

3. エラーハンドリングのカプセル化

共通のエラーハンドリングロジックをカプセル化することで、重複を避け、コードの再利用性を高めます。例えば、エラーハンドリングを行うユーティリティ関数を作成します。

async function handleError(asyncFunc) {
    try {
        return await asyncFunc();
    } catch (error) {
        logError(error);
        throw error; // 必要に応じて再スロー
    }
}

// 使用例
async function fetchData(url) {
    return await handleError(async () => {
        let response = await fetch(url);
        if (!response.ok) {
            throw new Error('ネットワーク応答が正しくありません');
        }
        return await response.json();
    });
}

4. ユーザーへのフィードバック

エラーが発生した場合、ユーザーに適切なフィードバックを提供することも重要です。ユーザーには、何が問題であったか、次に何をすればよいかを知らせるメッセージを表示します。

async function fetchData(url) {
    try {
        let response = await fetch(url);
        if (!response.ok) {
            throw new Error('ネットワーク応答が正しくありません');
        }
        return await response.json();
    } catch (error) {
        logError(error);
        alert('データの取得中に問題が発生しました。再度お試しください。');
        throw error; // 必要に応じて再スロー
    }
}

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

エラーハンドリングが正しく機能しているかを確認するために、ユニットテストや統合テストを行います。モックを使用して、様々なエラーシナリオをテストします。

// Jestを使用した例
test('fetchData handles network error', async () => {
    global.fetch = jest.fn(() =>
        Promise.reject(new Error('ネットワークエラー'))
    );

    try {
        await fetchData('https://api.example.com/data');
    } catch (error) {
        expect(error.message).toBe('ネットワークエラー');
    }
});

これらのベストプラクティスを実践することで、非同期処理におけるエラーハンドリングを効果的に管理し、アプリケーションの信頼性とユーザー体験を向上させることができます。次に、具体的なAPIコールでのエラーハンドリングの例を見ていきます。

具体例:APIコールでのエラーハンドリング

実際のプロジェクトでは、APIコールにおけるエラーハンドリングが非常に重要です。ここでは、APIコールを行う際のエラーハンドリングの具体例をいくつか示します。

基本的なAPIコールとエラーハンドリング

まず、基本的なAPIコールとそのエラーハンドリング方法を見ていきましょう。

async function fetchUserData(userId) {
    try {
        let response = await fetch(`https://api.example.com/users/${userId}`);
        if (!response.ok) {
            throw new Error(`HTTPエラー!ステータス:${response.status}`);
        }
        let data = await response.json();
        return data;
    } catch (error) {
        console.error('ユーザーデータの取得中にエラーが発生しました:', error);
        throw error;
    }
}

(async () => {
    try {
        let userData = await fetchUserData(1);
        console.log(userData);
    } catch (error) {
        console.error('fetchUserData関数がエラーを投げました:', error);
    }
})();

この例では、ユーザーデータを取得するためのAPIコールを行い、response.okを確認してエラーをスローしています。また、全体のエラーハンドリングをtry-catch構文で行っています。

リトライロジックの実装

APIコールが失敗した場合に再試行するリトライロジックを実装することで、一時的なネットワーク障害などに対処できます。

async function fetchWithRetry(url, options = {}, retries = 3, delay = 1000) {
    for (let i = 0; i < retries; i++) {
        try {
            let response = await fetch(url, options);
            if (!response.ok) {
                throw new Error(`HTTPエラー!ステータス:${response.status}`);
            }
            return await response.json();
        } catch (error) {
            if (i < retries - 1) {
                console.warn(`リトライ${i + 1}/${retries}:${error.message}`);
                await new Promise(resolve => setTimeout(resolve, delay));
            } else {
                console.error('リトライ失敗:', error);
                throw error;
            }
        }
    }
}

(async () => {
    try {
        let data = await fetchWithRetry('https://api.example.com/data');
        console.log(data);
    } catch (error) {
        console.error('fetchWithRetry関数がエラーを投げました:', error);
    }
})();

この例では、リトライの回数と間隔を指定し、APIコールが失敗した場合に指定された回数だけ再試行します。

タイムアウトの実装

APIコールが長時間かかる場合にタイムアウトを設定することで、適切にエラーを処理できます。

function fetchWithTimeout(url, options = {}, timeout = 5000) {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
            reject(new Error('タイムアウトエラー'));
        }, timeout);

        fetch(url, options).then(response => {
            clearTimeout(timer);
            if (!response.ok) {
                reject(new Error(`HTTPエラー!ステータス:${response.status}`));
            }
            return response.json().then(resolve).catch(reject);
        }).catch(error => {
            clearTimeout(timer);
            reject(error);
        });
    });
}

(async () => {
    try {
        let data = await fetchWithTimeout('https://api.example.com/data');
        console.log(data);
    } catch (error) {
        console.error('fetchWithTimeout関数がエラーを投げました:', error);
    }
})();

この例では、タイムアウトを設定し、指定された時間内にレスポンスが返ってこない場合にエラーをスローします。

エラーハンドリングのベストプラクティスの適用

前述のベストプラクティスを適用し、リトライロジックやタイムアウトを含む堅牢なAPIコール関数を作成します。

async function fetchData(url, options = {}) {
    try {
        return await fetchWithRetry(url, options);
    } catch (error) {
        logError(error);
        alert('データの取得中に問題が発生しました。再度お試しください。');
        throw error; // 必要に応じて再スロー
    }
}

(async () => {
    try {
        let data = await fetchData('https://api.example.com/data');
        console.log(data);
    } catch (error) {
        console.error('fetchData関数がエラーを投げました:', error);
    }
})();

この例では、リトライロジックとタイムアウトを統合し、エラーハンドリングのベストプラクティスを適用したAPIコール関数を実装しています。

これらの具体例を通じて、APIコールでのエラーハンドリングの方法を学び、実践することができます。次に、一般的なエラーの種類と対処法について説明します。

エラーの種類と対処法

非同期処理やAPIコールにおけるエラーにはさまざまな種類があり、それぞれ適切な対処法が必要です。ここでは、一般的なエラーの種類とその対処法について説明します。

ネットワークエラー

ネットワークエラーは、インターネット接続の問題やサーバーの応答がない場合に発生します。

async function fetchData(url) {
    try {
        let response = await fetch(url);
        if (!response.ok) {
            throw new Error(`HTTPエラー!ステータス:${response.status}`);
        }
        return await response.json();
    } catch (error) {
        if (error.message.includes('ネットワークエラー')) {
            console.error('ネットワークエラーが発生しました:', error);
            alert('ネットワークエラーが発生しました。接続を確認してください。');
        } else {
            throw error;
        }
    }
}

この例では、ネットワークエラーが発生した場合にユーザーに適切なフィードバックを提供します。

HTTPエラー

HTTPエラーは、サーバーから返されるHTTPステータスコードに基づいて発生します。特定のステータスコードに対して異なる処理を行うことが必要です。

async function fetchData(url) {
    try {
        let response = await fetch(url);
        if (!response.ok) {
            switch (response.status) {
                case 404:
                    throw new Error('リソースが見つかりません(404)');
                case 500:
                    throw new Error('サーバーエラー(500)');
                default:
                    throw new Error(`HTTPエラー!ステータス:${response.status}`);
            }
        }
        return await response.json();
    } catch (error) {
        console.error('HTTPエラーが発生しました:', error);
        alert(error.message);
        throw error;
    }
}

この例では、HTTPステータスコードに基づいてエラーメッセージをカスタマイズし、ユーザーに適切なフィードバックを提供します。

JSON解析エラー

JSON解析エラーは、サーバーからのレスポンスが有効なJSONでない場合に発生します。

async function fetchData(url) {
    try {
        let response = await fetch(url);
        if (!response.ok) {
            throw new Error(`HTTPエラー!ステータス:${response.status}`);
        }
        let data;
        try {
            data = await response.json();
        } catch (jsonError) {
            throw new Error('レスポンスのJSON解析に失敗しました');
        }
        return data;
    } catch (error) {
        console.error('エラーが発生しました:', error);
        alert('データの取得中にエラーが発生しました。');
        throw error;
    }
}

この例では、JSON解析エラーが発生した場合に適切にキャッチして処理します。

タイムアウトエラー

タイムアウトエラーは、指定した時間内にレスポンスが返ってこない場合に発生します。

function fetchWithTimeout(url, options = {}, timeout = 5000) {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
            reject(new Error('タイムアウトエラー'));
        }, timeout);

        fetch(url, options).then(response => {
            clearTimeout(timer);
            if (!response.ok) {
                reject(new Error(`HTTPエラー!ステータス:${response.status}`));
            }
            return response.json().then(resolve).catch(reject);
        }).catch(error => {
            clearTimeout(timer);
            reject(error);
        });
    });
}

(async () => {
    try {
        let data = await fetchWithTimeout('https://api.example.com/data');
        console.log(data);
    } catch (error) {
        if (error.message === 'タイムアウトエラー') {
            console.error('リクエストがタイムアウトしました:', error);
            alert('リクエストがタイムアウトしました。しばらくしてから再試行してください。');
        } else {
            console.error('fetchWithTimeout関数がエラーを投げました:', error);
        }
    }
})();

この例では、タイムアウトエラーが発生した場合に適切にキャッチして処理します。

カスタムエラー

独自のエラータイプを作成して使用することも有効です。これにより、特定の状況に対するエラーハンドリングがより明確になります。

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

async function fetchData(url) {
    try {
        let response = await fetch(url);
        if (!response.ok) {
            throw new CustomError(`カスタムエラー:ステータスコード ${response.status}`);
        }
        return await response.json();
    } catch (error) {
        if (error instanceof CustomError) {
            console.error('カスタムエラーが発生しました:', error);
        } else {
            console.error('一般的なエラーが発生しました:', error);
        }
        alert(error.message);
        throw error;
    }
}

(async () => {
    try {
        let data = await fetchData('https://api.example.com/data');
        console.log(data);
    } catch (error) {
        console.error('fetchData関数がエラーを投げました:', error);
    }
})();

この例では、カスタムエラーを作成して特定のエラーシナリオに対応しています。

これらのエラータイプに対する対処法を理解し、適切に実装することで、より堅牢で信頼性の高いアプリケーションを構築することができます。次に、エラーハンドリングのユニットテスト方法について説明します。

ユニットテストとエラーハンドリング

エラーハンドリングの正確性を保証するためには、ユニットテストが重要です。ユニットテストを通じて、コードが期待通りにエラーをキャッチし、適切に処理することを確認できます。ここでは、JavaScriptのテストフレームワークであるJestを使用してエラーハンドリングのユニットテストを実装する方法を説明します。

ユニットテストの準備

まず、Jestをインストールし、テスト環境を設定します。プロジェクトのルートディレクトリで以下のコマンドを実行します。

npm install --save-dev jest

次に、package.jsonファイルを編集し、テストスクリプトを追加します。

{
  "scripts": {
    "test": "jest"
  }
}

これで、npm testコマンドを実行することで、Jestを使用したテストが実行されます。

基本的なユニットテスト

まず、基本的なAPIコール関数に対するユニットテストを作成します。

// fetchData.js
async function fetchData(url) {
    let response = await fetch(url);
    if (!response.ok) {
        throw new Error(`HTTPエラー!ステータス:${response.status}`);
    }
    return await response.json();
}

module.exports = fetchData;

次に、この関数のテストを行います。

// fetchData.test.js
const fetchData = require('./fetchData');

global.fetch = jest.fn();

test('fetchData returns data when response is ok', async () => {
    const mockData = { id: 1, name: 'Test User' };
    fetch.mockResolvedValue({
        ok: true,
        json: async () => mockData
    });

    const data = await fetchData('https://api.example.com/users/1');
    expect(data).toEqual(mockData);
});

test('fetchData throws error when response is not ok', async () => {
    fetch.mockResolvedValue({
        ok: false,
        status: 404
    });

    await expect(fetchData('https://api.example.com/users/1')).rejects.toThrow('HTTPエラー!ステータス:404');
});

この例では、fetchData関数が正しいデータを返すことと、HTTPエラーが発生した場合に適切にエラーをスローすることを確認しています。

ネットワークエラーのテスト

ネットワークエラーに対するユニットテストを作成します。

test('fetchData throws error on network failure', async () => {
    fetch.mockRejectedValue(new Error('ネットワークエラー'));

    await expect(fetchData('https://api.example.com/users/1')).rejects.toThrow('ネットワークエラー');
});

このテストでは、ネットワークエラーが発生した場合にfetchData関数が正しくエラーをスローすることを確認します。

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

リトライロジックを含む関数に対するユニットテストを作成します。

// fetchWithRetry.js
async function fetchWithRetry(url, options = {}, retries = 3, delay = 1000) {
    for (let i = 0; i < retries; i++) {
        try {
            let response = await fetch(url, options);
            if (!response.ok) {
                throw new Error(`HTTPエラー!ステータス:${response.status}`);
            }
            return await response.json();
        } catch (error) {
            if (i < retries - 1) {
                await new Promise(resolve => setTimeout(resolve, delay));
            } else {
                throw error;
            }
        }
    }
}

module.exports = fetchWithRetry;
// fetchWithRetry.test.js
const fetchWithRetry = require('./fetchWithRetry');

global.fetch = jest.fn();

test('fetchWithRetry retries on failure', async () => {
    fetch.mockRejectedValueOnce(new Error('ネットワークエラー'));
    fetch.mockRejectedValueOnce(new Error('ネットワークエラー'));
    fetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ success: true })
    });

    const data = await fetchWithRetry('https://api.example.com/data');
    expect(data).toEqual({ success: true });
    expect(fetch).toHaveBeenCalledTimes(3);
});

test('fetchWithRetry throws error after max retries', async () => {
    fetch.mockRejectedValue(new Error('ネットワークエラー'));

    await expect(fetchWithRetry('https://api.example.com/data')).rejects.toThrow('ネットワークエラー');
    expect(fetch).toHaveBeenCalledTimes(3);
});

このテストでは、fetchWithRetry関数がリトライロジックを正しく実行し、最大リトライ回数に達した後にエラーをスローすることを確認します。

タイムアウトエラーのテスト

タイムアウトエラーに対するユニットテストを作成します。

// fetchWithTimeout.js
function fetchWithTimeout(url, options = {}, timeout = 5000) {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
            reject(new Error('タイムアウトエラー'));
        }, timeout);

        fetch(url, options).then(response => {
            clearTimeout(timer);
            if (!response.ok) {
                reject(new Error(`HTTPエラー!ステータス:${response.status}`));
            }
            return response.json().then(resolve).catch(reject);
        }).catch(error => {
            clearTimeout(timer);
            reject(error);
        });
    });
}

module.exports = fetchWithTimeout;
// fetchWithTimeout.test.js
const fetchWithTimeout = require('./fetchWithTimeout');

global.fetch = jest.fn();

test('fetchWithTimeout throws timeout error', async () => {
    fetch.mockResolvedValue(new Promise(resolve => setTimeout(() => resolve({ ok: true, json: async () => ({}) }), 6000)));

    await expect(fetchWithTimeout('https://api.example.com/data', {}, 5000)).rejects.toThrow('タイムアウトエラー');
});

このテストでは、fetchWithTimeout関数が指定された時間内にレスポンスを受け取らない場合にタイムアウトエラーをスローすることを確認します。

これらのユニットテストを通じて、エラーハンドリングが期待通りに動作することを保証し、コードの品質を高めることができます。次に、全体のまとめに移ります。

まとめ

本記事では、JavaScriptにおけるPromiseを使ったエラーハンドリングの基本から応用までを詳しく解説しました。非同期処理の重要性を理解し、Promiseやasync/awaitを利用したエラーハンドリングの方法を学ぶことで、コードの信頼性と可読性を向上させることができます。

具体的には、Promiseの基本構文やチェーン内でのエラーハンドリング、async/awaitを用いたエラーハンドリングの方法、そしてリトライロジックやタイムアウトの実装方法を紹介しました。また、APIコールにおけるエラーハンドリングの具体例や、一般的なエラーの種類と対処法についても説明しました。

さらに、エラーハンドリングのベストプラクティスを実践し、ユニットテストを通じてエラーハンドリングが正しく機能することを確認する方法も学びました。

これらの知識を活用することで、JavaScriptの非同期処理を効果的に管理し、より堅牢で信頼性の高いアプリケーションを構築することができるでしょう。

コメント

コメントする

目次