JavaScriptで学ぶネットワークエラーの処理方法とベストプラクティス

JavaScriptでのネットワークエラー処理は、Webアプリケーションの信頼性とユーザーエクスペリエンスを向上させるために不可欠です。ネットワークエラーは、サーバーの応答がない、インターネット接続が切れた、リクエストがタイムアウトしたなど、さまざまな原因で発生します。これらのエラーを適切に処理し、ユーザーに分かりやすいフィードバックを提供することは、アプリケーションの健全性を保つ上で重要です。本記事では、JavaScriptを使用したネットワークエラーの処理方法と、その具体的な実装例を詳しく解説します。初心者から上級者まで、すべての開発者が役立つ情報を提供します。

目次
  1. ネットワークエラーとは何か
    1. HTTPステータスコードによるエラー
    2. ネットワーク接続の問題
    3. クライアント側のエラー
  2. ネットワークエラーの発生原因
    1. 1. サーバー側の問題
    2. 2. クライアント側の問題
    3. 3. ネットワークインフラの問題
    4. 4. ソフトウェアのバグ
  3. JavaScriptのエラーハンドリング基礎
    1. try-catch構文
    2. finallyブロック
    3. エラーオブジェクト
    4. カスタムエラーの作成
  4. Fetch APIでのエラーハンドリング
    1. 基本的なFetchの使い方
    2. HTTPステータスコードの確認
    3. ネットワークエラーの処理
    4. タイムアウトの設定
  5. Axiosを使ったエラーハンドリング
    1. Axiosの基本的な使い方
    2. HTTPステータスコードの確認
    3. ネットワークエラーの処理
    4. タイムアウトの設定
    5. リトライ機能の実装
    6. グローバルなエラーハンドリング
  6. エラーメッセージの表示方法
    1. エラーメッセージの基本原則
    2. JavaScriptでのエラーメッセージ表示例
    3. ユーザーに役立つエラーメッセージ
    4. エラーメッセージのスタイリング
  7. リトライ機能の実装
    1. 基本的なリトライロジック
    2. 指数バックオフの実装
    3. リトライ条件のカスタマイズ
  8. タイムアウト処理
    1. Fetch APIでのタイムアウト処理
    2. Axiosでのタイムアウト処理
    3. タイムアウトとリトライの組み合わせ
    4. ユーザーへのフィードバック
  9. オフライン状態の検出と処理
    1. オフライン状態の検出
    2. オフライン時のUI更新
    3. オフライン時のデータ保存と同期
  10. ユーザーフレンドリーなエラーハンドリング
    1. エラーメッセージの明確さ
    2. ユーザー行動に基づくエラー処理
    3. エラーアイコンや色の使用
    4. コンテキストに応じたエラーハンドリング
  11. テストとデバッグ
    1. ネットワークエラーのテスト手法
    2. エラーハンドリングのユニットテスト
    3. デバッグのポイント
  12. まとめ

ネットワークエラーとは何か

ネットワークエラーとは、クライアントとサーバー間の通信が正常に行われない場合に発生する問題のことを指します。これには、以下のような種類が含まれます。

HTTPステータスコードによるエラー

HTTPステータスコードは、サーバーからクライアントに返されるレスポンスの状態を示します。エラーを示すステータスコードには以下が含まれます。

  • 400 Bad Request: クライアントからのリクエストが不正である場合に返されます。
  • 401 Unauthorized: 認証が必要なリソースにアクセスしようとした際に返されます。
  • 404 Not Found: リクエストしたリソースが見つからない場合に返されます。
  • 500 Internal Server Error: サーバー内部でエラーが発生した場合に返されます。

ネットワーク接続の問題

インターネット接続の問題やサーバーがダウンしている場合、クライアントはサーバーに到達できず、リクエストが失敗します。これには以下が含まれます。

  • DNSエラー: ドメイン名が解決できない場合に発生します。
  • タイムアウト: サーバーからの応答が規定の時間内に得られない場合に発生します。

クライアント側のエラー

クライアントの設定や環境により発生するエラーもあります。これには以下が含まれます。

  • ブラウザ設定の問題: JavaScriptが無効になっている場合や、CORSポリシーに違反している場合。
  • ローカルネットワークの問題: Wi-Fi接続の不具合やプロキシ設定の問題。

ネットワークエラーを正確に理解し、その発生原因を特定することは、適切なエラーハンドリングを行うための第一歩です。

ネットワークエラーの発生原因

ネットワークエラーはさまざまな原因で発生し、クライアントとサーバー間の通信を妨げます。以下では、一般的なネットワークエラーの原因とその背景について詳しく説明します。

1. サーバー側の問題

サーバー側で発生する問題は、ネットワークエラーの主要な原因の一つです。これには以下のようなケースが含まれます。

1.1 サーバーダウン

サーバーが停止している、またはクラッシュしている場合、クライアントのリクエストに応答できません。

1.2 サーバー過負荷

サーバーが過負荷状態にある場合、リクエストの処理が遅延したり、失敗したりすることがあります。

1.3 サーバー設定ミス

サーバーの設定ミスや不適切な構成も、ネットワークエラーの原因となります。

2. クライアント側の問題

クライアントの環境や設定もネットワークエラーの原因となります。これには以下のケースが含まれます。

2.1 インターネット接続の問題

クライアントのインターネット接続が不安定、または切断されている場合、リクエストが送信できません。

2.2 ブラウザ設定の問題

ブラウザの設定でJavaScriptが無効になっている場合や、CORSポリシーが原因でリクエストがブロックされることがあります。

2.3 ローカルネットワークの問題

Wi-Fiの接続不良やプロキシ設定の誤りも、ネットワークエラーの原因になります。

3. ネットワークインフラの問題

インターネット自体の問題も、ネットワークエラーを引き起こす可能性があります。

3.1 DNSの問題

ドメイン名の解決ができない場合、リクエストが送信できません。

3.2 ルーターやスイッチの故障

ネットワーク機器の障害や設定ミスが原因で、通信が途絶えることがあります。

4. ソフトウェアのバグ

ソフトウェアやアプリケーションのバグもネットワークエラーの原因となります。

4.1 クライアント側のバグ

アプリケーションコードのバグにより、リクエストが正しく送信されないことがあります。

4.2 サーバー側のバグ

サーバーのソフトウェアバグにより、リクエストの処理が失敗することがあります。

これらの原因を理解し、適切な対策を講じることで、ネットワークエラーを効果的に防止・処理することが可能です。

JavaScriptのエラーハンドリング基礎

JavaScriptにおけるエラーハンドリングは、アプリケーションの安定性とユーザーエクスペリエンスを向上させるために重要です。基本的なエラーハンドリングの方法を理解することで、予期しないエラーが発生した場合でも、適切に対処できます。

try-catch構文

JavaScriptでは、エラーが発生しそうなコードをtryブロックに入れ、エラーが発生した場合にcatchブロックでそのエラーをキャッチして処理します。以下は基本的な例です。

try {
    // エラーが発生する可能性のあるコード
    let result = riskyOperation();
    console.log(result);
} catch (error) {
    // エラーが発生した場合の処理
    console.error('エラーが発生しました:', error.message);
}

tryブロック

tryブロック内には、エラーが発生する可能性のあるコードを記述します。ここに書かれたコードが正常に実行されれば、catchブロックはスキップされます。

catchブロック

catchブロックは、tryブロック内でエラーが発生した場合に実行されます。引数としてエラーオブジェクトを受け取り、その情報を基にエラー処理を行います。

finallyブロック

finallyブロックは、エラーの有無にかかわらず必ず実行されるコードを記述するために使用されます。例えば、リソースの解放や後処理を行う場合に便利です。

try {
    let result = riskyOperation();
    console.log(result);
} catch (error) {
    console.error('エラーが発生しました:', error.message);
} finally {
    console.log('後処理を行います');
}

エラーオブジェクト

catchブロックのエラーオブジェクトには、エラーに関する詳細な情報が含まれています。主に以下のプロパティが使用されます。

  • name: エラーの種類を示す名前
  • message: エラーの詳細なメッセージ

例:

try {
    throw new Error('意図的なエラー');
} catch (error) {
    console.log(error.name); // "Error"
    console.log(error.message); // "意図的なエラー"
}

カスタムエラーの作成

独自のエラーメッセージを持つカスタムエラーを作成することも可能です。これにより、特定のエラー状況に対してより詳細な情報を提供できます。

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

try {
    throw new CustomError('カスタムエラーが発生しました');
} catch (error) {
    console.error(error.name); // "CustomError"
    console.error(error.message); // "カスタムエラーが発生しました"
}

基本的なエラーハンドリングを理解し、適切に実装することで、JavaScriptアプリケーションの信頼性とユーザーエクスペリエンスを向上させることができます。

Fetch APIでのエラーハンドリング

Fetch APIは、JavaScriptでHTTPリクエストを行うためのモダンな方法です。Fetch APIを使用することで、ネットワークリクエストをシンプルに実装できますが、適切なエラーハンドリングも重要です。

基本的なFetchの使い方

Fetch APIは、fetch関数を使用してリクエストを行い、Promiseオブジェクトを返します。基本的な使い方は次の通りです。

fetch('https://api.example.com/data')
    .then(response => response.json())
    .then(data => {
        console.log('データを取得しました:', data);
    })
    .catch(error => {
        console.error('エラーが発生しました:', error);
    });

HTTPステータスコードの確認

Fetch APIは、ステータスコードが400や500などのエラーレスポンスでもPromiseを解決します。そのため、レスポンスのステータスコードを明示的に確認する必要があります。

fetch('https://api.example.com/data')
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTPエラー: ${response.status}`);
        }
        return response.json();
    })
    .then(data => {
        console.log('データを取得しました:', data);
    })
    .catch(error => {
        console.error('エラーが発生しました:', error);
    });

ネットワークエラーの処理

ネットワークエラー(例:ネットワークがオフラインの場合)も適切に処理する必要があります。これには、fetch関数自体が失敗した場合のエラーハンドリングが含まれます。

fetch('https://api.example.com/data')
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTPエラー: ${response.status}`);
        }
        return response.json();
    })
    .then(data => {
        console.log('データを取得しました:', data);
    })
    .catch(error => {
        if (error.name === 'TypeError') {
            console.error('ネットワークエラーが発生しました:', error.message);
        } else {
            console.error('その他のエラーが発生しました:', error.message);
        }
    });

タイムアウトの設定

Fetch API自体にはタイムアウトの設定がありませんが、Promiseを使ってタイムアウトを実装できます。以下の例では、タイムアウト時間を設定し、その時間内にレスポンスが得られない場合にエラーをスローします。

const fetchWithTimeout = (url, options, timeout = 5000) => {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
            reject(new Error('リクエストがタイムアウトしました'));
        }, timeout);

        fetch(url, options)
            .then(response => {
                clearTimeout(timer);
                resolve(response);
            })
            .catch(error => {
                clearTimeout(timer);
                reject(error);
            });
    });
};

fetchWithTimeout('https://api.example.com/data')
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTPエラー: ${response.status}`);
        }
        return response.json();
    })
    .then(data => {
        console.log('データを取得しました:', data);
    })
    .catch(error => {
        console.error('エラーが発生しました:', error);
    });

Fetch APIを使ったエラーハンドリングを適切に実装することで、ネットワークリクエストが失敗した場合でも、アプリケーションが安定して動作するようになります。

Axiosを使ったエラーハンドリング

Axiosは、JavaScriptでHTTPリクエストを行うための人気の高いライブラリです。Axiosを使用することで、より高度なエラーハンドリングを簡単に実装できます。ここでは、Axiosを使ったネットワークエラーの処理方法について解説します。

Axiosの基本的な使い方

まずは、Axiosを使用して基本的なHTTPリクエストを行う方法を見てみましょう。

const axios = require('axios');

axios.get('https://api.example.com/data')
    .then(response => {
        console.log('データを取得しました:', response.data);
    })
    .catch(error => {
        console.error('エラーが発生しました:', error);
    });

HTTPステータスコードの確認

Axiosはレスポンスのステータスコードが200以外の場合、自動的にエラーをスローします。これにより、エラー処理がシンプルになります。

axios.get('https://api.example.com/data')
    .then(response => {
        console.log('データを取得しました:', response.data);
    })
    .catch(error => {
        if (error.response) {
            // サーバーがステータスコードを返したが、範囲外(2xxではない)
            console.error('ステータスコードエラー:', error.response.status);
            console.error('レスポンスデータ:', error.response.data);
        } else if (error.request) {
            // リクエストは送信されたが、レスポンスがない
            console.error('レスポンスなし:', error.request);
        } else {
            // リクエストの設定時にエラーが発生
            console.error('リクエストエラー:', error.message);
        }
    });

ネットワークエラーの処理

Axiosは、ネットワークエラーも適切に処理するためのツールを提供しています。これには、リクエストが送信されない場合や、タイムアウトが発生した場合の処理が含まれます。

axios.get('https://api.example.com/data')
    .then(response => {
        console.log('データを取得しました:', response.data);
    })
    .catch(error => {
        if (error.response) {
            console.error('ステータスコードエラー:', error.response.status);
        } else if (error.request) {
            console.error('ネットワークエラー:', error.message);
        } else {
            console.error('リクエストエラー:', error.message);
        }
    });

タイムアウトの設定

Axiosでは、リクエストごとにタイムアウトを設定することができます。タイムアウトが設定された場合、その時間内にレスポンスが得られなければエラーをスローします。

axios.get('https://api.example.com/data', { timeout: 5000 })
    .then(response => {
        console.log('データを取得しました:', response.data);
    })
    .catch(error => {
        if (error.code === 'ECONNABORTED') {
            console.error('リクエストがタイムアウトしました:', error.message);
        } else {
            console.error('その他のエラーが発生しました:', error.message);
        }
    });

リトライ機能の実装

ネットワークリクエストが失敗した場合にリトライを行うことで、信頼性を向上させることができます。以下は、失敗したリクエストをリトライする例です。

const axiosRetry = require('axios-retry');

axiosRetry(axios, { retries: 3 });

axios.get('https://api.example.com/data')
    .then(response => {
        console.log('データを取得しました:', response.data);
    })
    .catch(error => {
        console.error('エラーが発生しました:', error.message);
    });

グローバルなエラーハンドリング

アプリケーション全体で共通のエラーハンドリングを行うために、Axiosインターセプターを使用することができます。

axios.interceptors.response.use(
    response => response,
    error => {
        console.error('共通のエラー処理:', error.message);
        return Promise.reject(error);
    }
);

axios.get('https://api.example.com/data')
    .then(response => {
        console.log('データを取得しました:', response.data);
    })
    .catch(error => {
        // インターセプターによってエラーが処理される
    });

Axiosを使ったエラーハンドリングを適切に実装することで、ネットワークリクエストが失敗した場合でも、アプリケーションが安定して動作し、ユーザーにわかりやすいフィードバックを提供できます。

エラーメッセージの表示方法

ネットワークリクエストが失敗した場合、ユーザーにわかりやすいエラーメッセージを表示することが重要です。適切なエラーメッセージを表示することで、ユーザーは問題の原因を理解し、次のアクションを決定する手助けができます。

エラーメッセージの基本原則

エラーメッセージを表示する際には、以下の基本原則を守ることが重要です。

1. 簡潔かつ具体的

エラーメッセージは簡潔でありながら、問題の内容を具体的に伝える必要があります。

2. ユーザーフレンドリー

技術的な用語を避け、ユーザーが理解しやすい言葉で表現します。

3. 解決策を提示

可能であれば、ユーザーが問題を解決するための次のステップを示します。

JavaScriptでのエラーメッセージ表示例

以下は、JavaScriptを使用してエラーメッセージを表示する基本的な方法です。ここでは、HTML要素を操作してエラーメッセージを表示します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>エラーメッセージの表示</title>
</head>
<body>
    <div id="error-message" style="display:none; color: red;"></div>
    <script>
        function showError(message) {
            const errorMessageElement = document.getElementById('error-message');
            errorMessageElement.textContent = message;
            errorMessageElement.style.display = 'block';
        }

        // エラーハンドリングの例
        fetch('https://api.example.com/data')
            .then(response => {
                if (!response.ok) {
                    throw new Error(`HTTPエラー: ${response.status}`);
                }
                return response.json();
            })
            .then(data => {
                console.log('データを取得しました:', data);
            })
            .catch(error => {
                showError('データの取得中にエラーが発生しました。もう一度お試しください。');
                console.error('エラーの詳細:', error);
            });
    </script>
</body>
</html>

ユーザーに役立つエラーメッセージ

エラーメッセージは、単に「エラーが発生しました」ではなく、ユーザーが次に何をすべきかを示す情報を含めると効果的です。例えば、ネットワーク接続が切れている場合は、以下のようなメッセージを表示します。

function handleError(error) {
    let userMessage;
    if (error.message.includes('NetworkError')) {
        userMessage = 'ネットワークに接続されていません。接続を確認して再試行してください。';
    } else if (error.response && error.response.status === 404) {
        userMessage = 'リクエストしたリソースが見つかりません。URLを確認してください。';
    } else {
        userMessage = 'エラーが発生しました。もう一度お試しください。';
    }
    showError(userMessage);
}

fetch('https://api.example.com/data')
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTPエラー: ${response.status}`);
        }
        return response.json();
    })
    .then(data => {
        console.log('データを取得しました:', data);
    })
    .catch(error => {
        handleError(error);
        console.error('エラーの詳細:', error);
    });

エラーメッセージのスタイリング

ユーザーにとって見やすいエラーメッセージを表示するために、CSSを用いてスタイルを設定することも重要です。以下は、エラーメッセージのスタイルを設定する例です。

<style>
    #error-message {
        display: none;
        color: red;
        background-color: #f8d7da;
        border: 1px solid #f5c6cb;
        padding: 10px;
        margin: 10px 0;
        border-radius: 5px;
    }
</style>

エラーメッセージの適切な表示方法を理解し、実装することで、ユーザーに対してより良いエクスペリエンスを提供できます。また、ユーザーが問題を解決しやすくするための手助けとなります。

リトライ機能の実装

ネットワークリクエストが失敗した場合、リトライ機能を実装することで、信頼性を向上させることができます。特に、一時的なネットワーク障害やサーバーの過負荷状態など、再試行すれば成功する可能性がある場合に有効です。ここでは、JavaScriptでリトライ機能を実装する方法を解説します。

基本的なリトライロジック

リトライ機能を実装するための基本的なロジックは、リクエストが失敗した場合に指定回数だけ再試行するというものです。以下は、Fetch APIを使用したシンプルなリトライロジックの例です。

function fetchWithRetry(url, options = {}, retries = 3, backoff = 300) {
    return new Promise((resolve, reject) => {
        const attemptFetch = (n) => {
            fetch(url, options)
                .then(response => {
                    if (!response.ok) {
                        throw new Error(`HTTPエラー: ${response.status}`);
                    }
                    return response.json();
                })
                .then(data => resolve(data))
                .catch(error => {
                    if (n === 0) {
                        reject(error);
                    } else {
                        setTimeout(() => {
                            console.log(`リトライ残り回数: ${n}`);
                            attemptFetch(n - 1);
                        }, backoff);
                    }
                });
        };
        attemptFetch(retries);
    });
}

fetchWithRetry('https://api.example.com/data')
    .then(data => {
        console.log('データを取得しました:', data);
    })
    .catch(error => {
        console.error('全てのリトライが失敗しました:', error);
    });

指数バックオフの実装

リトライを行う際、単純に同じ時間間隔で再試行するのではなく、指数バックオフを使用することで、サーバーへの負荷を軽減し、成功する確率を高めることができます。指数バックオフでは、リトライの間隔を指数関数的に増加させます。

function fetchWithExponentialBackoff(url, options = {}, retries = 3, backoff = 300) {
    return new Promise((resolve, reject) => {
        const attemptFetch = (n, delay) => {
            fetch(url, options)
                .then(response => {
                    if (!response.ok) {
                        throw new Error(`HTTPエラー: ${response.status}`);
                    }
                    return response.json();
                })
                .then(data => resolve(data))
                .catch(error => {
                    if (n === 0) {
                        reject(error);
                    } else {
                        setTimeout(() => {
                            console.log(`リトライ残り回数: ${n}, 次回リトライまでの待機時間: ${delay}ms`);
                            attemptFetch(n - 1, delay * 2);
                        }, delay);
                    }
                });
        };
        attemptFetch(retries, backoff);
    });
}

fetchWithExponentialBackoff('https://api.example.com/data')
    .then(data => {
        console.log('データを取得しました:', data);
    })
    .catch(error => {
        console.error('全てのリトライが失敗しました:', error);
    });

リトライ条件のカスタマイズ

すべてのエラーでリトライを行うわけではなく、特定の条件に基づいてリトライを行うことも重要です。例えば、ネットワークエラーや特定のHTTPステータスコードに対してのみリトライを実施するようにします。

function fetchWithCustomRetry(url, options = {}, retries = 3, backoff = 300) {
    return new Promise((resolve, reject) => {
        const attemptFetch = (n, delay) => {
            fetch(url, options)
                .then(response => {
                    if (!response.ok) {
                        if (response.status >= 500 && response.status < 600) {
                            throw new Error(`サーバーエラー: ${response.status}`);
                        } else {
                            reject(new Error(`HTTPエラー: ${response.status}`));
                        }
                    }
                    return response.json();
                })
                .then(data => resolve(data))
                .catch(error => {
                    if (n === 0 || !(error.message.includes('サーバーエラー') || error.message.includes('NetworkError'))) {
                        reject(error);
                    } else {
                        setTimeout(() => {
                            console.log(`リトライ残り回数: ${n}, 次回リトライまでの待機時間: ${delay}ms`);
                            attemptFetch(n - 1, delay * 2);
                        }, delay);
                    }
                });
        };
        attemptFetch(retries, backoff);
    });
}

fetchWithCustomRetry('https://api.example.com/data')
    .then(data => {
        console.log('データを取得しました:', data);
    })
    .catch(error => {
        console.error('全てのリトライが失敗しました:', error);
    });

リトライ機能を適切に実装することで、一時的なエラーが発生してもアプリケーションの信頼性を維持し、ユーザーエクスペリエンスを向上させることができます。

タイムアウト処理

ネットワークリクエストにタイムアウト処理を追加することで、リクエストが無限に待機状態になるのを防ぎ、ユーザーに迅速なフィードバックを提供できます。タイムアウトは、特定の時間内にレスポンスが得られない場合にエラーハンドリングを実行する仕組みです。

Fetch APIでのタイムアウト処理

Fetch APIにはデフォルトでタイムアウト機能がないため、Promiseを使ってタイムアウトを実装します。以下の例では、Promise.raceを使用してタイムアウトを設定します。

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) {
                    throw new Error(`HTTPエラー: ${response.status}`);
                }
                return response.json();
            })
            .then(data => resolve(data))
            .catch(error => {
                clearTimeout(timer);
                reject(error);
            });
    });
}

fetchWithTimeout('https://api.example.com/data')
    .then(data => {
        console.log('データを取得しました:', data);
    })
    .catch(error => {
        console.error('エラーが発生しました:', error.message);
    });

Axiosでのタイムアウト処理

Axiosでは、タイムアウト設定が簡単に行えます。リクエストのオプションとしてtimeoutプロパティを指定するだけです。

const axios = require('axios');

axios.get('https://api.example.com/data', { timeout: 5000 })
    .then(response => {
        console.log('データを取得しました:', response.data);
    })
    .catch(error => {
        if (error.code === 'ECONNABORTED') {
            console.error('リクエストがタイムアウトしました:', error.message);
        } else {
            console.error('その他のエラーが発生しました:', error.message);
        }
    });

タイムアウトとリトライの組み合わせ

タイムアウト処理とリトライ機能を組み合わせることで、より堅牢なエラーハンドリングを実現できます。以下の例では、Fetch APIを使用してタイムアウトとリトライを実装しています。

function fetchWithTimeoutAndRetry(url, options = {}, timeout = 5000, retries = 3, backoff = 300) {
    return new Promise((resolve, reject) => {
        const attemptFetch = (n, delay) => {
            const timer = setTimeout(() => {
                reject(new Error('リクエストがタイムアウトしました'));
            }, timeout);

            fetch(url, options)
                .then(response => {
                    clearTimeout(timer);
                    if (!response.ok) {
                        throw new Error(`HTTPエラー: ${response.status}`);
                    }
                    return response.json();
                })
                .then(data => resolve(data))
                .catch(error => {
                    clearTimeout(timer);
                    if (n === 0 || !(error.message.includes('タイムアウト') || error.message.includes('NetworkError'))) {
                        reject(error);
                    } else {
                        setTimeout(() => {
                            console.log(`リトライ残り回数: ${n}, 次回リトライまでの待機時間: ${delay}ms`);
                            attemptFetch(n - 1, delay * 2);
                        }, delay);
                    }
                });
        };
        attemptFetch(retries, backoff);
    });
}

fetchWithTimeoutAndRetry('https://api.example.com/data')
    .then(data => {
        console.log('データを取得しました:', data);
    })
    .catch(error => {
        console.error('全てのリトライが失敗しました:', error.message);
    });

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

タイムアウトが発生した場合、ユーザーに適切なフィードバックを提供することが重要です。例えば、ローディングスピナーを表示し、一定時間後にエラーメッセージを表示する方法があります。

function showError(message) {
    const errorMessageElement = document.getElementById('error-message');
    errorMessageElement.textContent = message;
    errorMessageElement.style.display = 'block';
}

function showLoading() {
    const loadingElement = document.getElementById('loading');
    loadingElement.style.display = 'block';
}

function hideLoading() {
    const loadingElement = document.getElementById('loading');
    loadingElement.style.display = 'none';
}

document.getElementById('fetch-data').addEventListener('click', () => {
    showLoading();
    fetchWithTimeout('https://api.example.com/data', {}, 5000)
        .then(data => {
            console.log('データを取得しました:', data);
        })
        .catch(error => {
            showError('データの取得に失敗しました。もう一度お試しください。');
            console.error('エラーの詳細:', error);
        })
        .finally(() => {
            hideLoading();
        });
});

このように、タイムアウト処理を適切に実装し、ユーザーにわかりやすいフィードバックを提供することで、アプリケーションの信頼性とユーザーエクスペリエンスを向上させることができます。

オフライン状態の検出と処理

Webアプリケーションにおいて、ユーザーがオフライン状態であることを検出し、適切な処理を行うことは重要です。JavaScriptでは、ブラウザのNavigatorオブジェクトを使用して、ネットワーク接続状態を監視することができます。ここでは、オフライン状態を検出し、その状態に応じた処理方法を解説します。

オフライン状態の検出

JavaScriptでは、NavigatorオブジェクトのonLineプロパティを使用して、ユーザーがオンラインかオフラインかを検出することができます。また、onlineおよびofflineイベントをリッスンして、接続状態の変化を監視することもできます。

function updateOnlineStatus() {
    const statusElement = document.getElementById('status');
    if (navigator.onLine) {
        statusElement.textContent = 'オンライン';
        statusElement.style.color = 'green';
    } else {
        statusElement.textContent = 'オフライン';
        statusElement.style.color = 'red';
    }
}

window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);

// ページロード時に初期状態を設定
updateOnlineStatus();

オフライン時のUI更新

ユーザーがオフラインになった際に、適切なメッセージやオフラインモードを表示することで、ユーザーエクスペリエンスを向上させることができます。以下は、オフライン状態を通知するUIの例です。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ネットワーク状態の検出</title>
    <style>
        #status {
            padding: 10px;
            font-size: 20px;
        }
        .offline-alert {
            display: none;
            background-color: #ffcc00;
            color: black;
            padding: 10px;
            margin: 10px 0;
            border: 1px solid #ff9900;
        }
    </style>
</head>
<body>
    <div id="status"></div>
    <div id="offline-alert" class="offline-alert">現在オフラインです。ネットワーク接続を確認してください。</div>
    <script>
        function updateOnlineStatus() {
            const statusElement = document.getElementById('status');
            const offlineAlertElement = document.getElementById('offline-alert');
            if (navigator.onLine) {
                statusElement.textContent = 'オンライン';
                statusElement.style.color = 'green';
                offlineAlertElement.style.display = 'none';
            } else {
                statusElement.textContent = 'オフライン';
                statusElement.style.color = 'red';
                offlineAlertElement.style.display = 'block';
            }
        }

        window.addEventListener('online', updateOnlineStatus);
        window.addEventListener('offline', updateOnlineStatus);

        // ページロード時に初期状態を設定
        updateOnlineStatus();
    </script>
</body>
</html>

オフライン時のデータ保存と同期

ユーザーがオフラインのときに入力したデータを保存し、オンラインに戻ったときに同期することで、シームレスなユーザーエクスペリエンスを提供できます。このために、localStorageIndexedDBを使用してデータをローカルに保存し、オンライン時にサーバーと同期します。

function saveDataLocally(data) {
    const dataList = JSON.parse(localStorage.getItem('dataList')) || [];
    dataList.push(data);
    localStorage.setItem('dataList', JSON.stringify(dataList));
}

function syncData() {
    const dataList = JSON.parse(localStorage.getItem('dataList'));
    if (dataList && dataList.length > 0) {
        dataList.forEach(data => {
            // サーバーにデータを送信
            fetch('https://api.example.com/data', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(data)
            })
            .then(response => {
                if (response.ok) {
                    console.log('データが同期されました:', data);
                    // 同期が成功したデータをローカルストレージから削除
                    const index = dataList.indexOf(data);
                    if (index > -1) {
                        dataList.splice(index, 1);
                    }
                    localStorage.setItem('dataList', JSON.stringify(dataList));
                }
            })
            .catch(error => {
                console.error('データの同期に失敗しました:', error);
            });
        });
    }
}

window.addEventListener('online', syncData);

// オフライン時のデータ保存例
document.getElementById('save-button').addEventListener('click', () => {
    const data = { /* 保存するデータ */ };
    saveDataLocally(data);
});

このように、オフライン状態を検出し、適切な処理を行うことで、ユーザーがネットワーク接続の状態に関係なくスムーズにアプリケーションを使用できるようにすることができます。

ユーザーフレンドリーなエラーハンドリング

エラーハンドリングは単にエラーを捕捉して処理するだけでなく、ユーザーにとってわかりやすく、役立つフィードバックを提供することも重要です。ユーザーフレンドリーなエラーハンドリングを実装することで、ユーザーエクスペリエンスを大幅に向上させることができます。

エラーメッセージの明確さ

エラーメッセージは簡潔で明確にする必要があります。技術的な詳細は避け、ユーザーが理解できる言葉で説明します。具体的なアクションを示すと、ユーザーが問題を解決しやすくなります。

function showError(message) {
    const errorMessageElement = document.getElementById('error-message');
    errorMessageElement.textContent = message;
    errorMessageElement.style.display = 'block';
}

// エラーハンドリングの例
fetch('https://api.example.com/data')
    .then(response => {
        if (!response.ok) {
            throw new Error(`サーバーエラー: ${response.status}`);
        }
        return response.json();
    })
    .then(data => {
        console.log('データを取得しました:', data);
    })
    .catch(error => {
        if (navigator.onLine) {
            showError('データの取得に失敗しました。再試行してください。');
        } else {
            showError('オフライン状態です。インターネット接続を確認してください。');
        }
        console.error('エラーの詳細:', error);
    });

ユーザー行動に基づくエラー処理

ユーザーが特定の操作を行った際にエラーが発生した場合、その操作に関連するエラーメッセージを表示します。例えば、フォームの送信エラーの場合、入力フィールドの近くにエラーメッセージを表示します。

document.getElementById('submit-button').addEventListener('click', (event) => {
    event.preventDefault();
    const formData = new FormData(document.getElementById('my-form'));

    fetch('https://api.example.com/submit', {
        method: 'POST',
        body: formData
    })
    .then(response => {
        if (!response.ok) {
            throw new Error(`サーバーエラー: ${response.status}`);
        }
        return response.json();
    })
    .then(data => {
        console.log('フォームが送信されました:', data);
    })
    .catch(error => {
        const errorMessageElement = document.getElementById('form-error-message');
        if (navigator.onLine) {
            errorMessageElement.textContent = 'フォームの送信に失敗しました。再試行してください。';
        } else {
            errorMessageElement.textContent = 'オフライン状態です。インターネット接続を確認してください。';
        }
        errorMessageElement.style.display = 'block';
        console.error('エラーの詳細:', error);
    });
});

エラーアイコンや色の使用

エラーメッセージを視覚的に強調するために、アイコンや色を使用します。赤色はエラーを示すためによく使われます。

<style>
    .error-message {
        display: none;
        color: red;
        background-color: #f8d7da;
        border: 1px solid #f5c6cb;
        padding: 10px;
        margin: 10px 0;
        border-radius: 5px;
    }
</style>

<div id="error-message" class="error-message">
    <span>&#9888;</span> <span id="error-text"></span>
</div>
function showError(message) {
    const errorMessageElement = document.getElementById('error-message');
    const errorTextElement = document.getElementById('error-text');
    errorTextElement.textContent = message;
    errorMessageElement.style.display = 'block';
}

コンテキストに応じたエラーハンドリング

エラーが発生した状況に応じて、適切なエラーメッセージを表示します。例えば、サーバーエラー、クライアントエラー、ネットワークエラーなどに対して異なるメッセージを表示します。

fetch('https://api.example.com/data')
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTPエラー: ${response.status}`);
        }
        return response.json();
    })
    .then(data => {
        console.log('データを取得しました:', data);
    })
    .catch(error => {
        let errorMessage;
        if (error.message.includes('HTTPエラー')) {
            errorMessage = 'サーバーに問題があります。後でもう一度お試しください。';
        } else if (navigator.onLine) {
            errorMessage = 'データの取得に失敗しました。再試行してください。';
        } else {
            errorMessage = 'オフライン状態です。インターネット接続を確認してください。';
        }
        showError(errorMessage);
        console.error('エラーの詳細:', error);
    });

ユーザーフレンドリーなエラーハンドリングを実装することで、エラー発生時にもユーザーにとってストレスの少ない体験を提供できます。エラーメッセージは明確で具体的にし、適切なビジュアルフィードバックを提供することが重要です。

テストとデバッグ

ネットワークエラー処理のテストとデバッグは、アプリケーションの信頼性と安定性を確保するために重要です。エラーハンドリングが適切に機能していることを確認するために、さまざまなシナリオをテストし、デバッグを行う必要があります。

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

ネットワークエラーのテストは、実際のエラー状況をシミュレーションすることで行います。以下のような手法があります。

1. モックサーバーの使用

モックサーバーを使用して、特定のHTTPステータスコードやレスポンスを返すことで、さまざまなエラーシナリオをテストします。例えば、json-servernockなどのライブラリを使用できます。

const nock = require('nock');

nock('https://api.example.com')
    .get('/data')
    .reply(500, { message: 'Internal Server Error' });

fetch('https://api.example.com/data')
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTPエラー: ${response.status}`);
        }
        return response.json();
    })
    .catch(error => {
        console.error('テストエラー:', error.message);
    });

2. ネットワーク条件のシミュレーション

開発者ツールを使用して、ネットワークの速度を制限したり、オフライン状態をシミュレーションすることで、エラーハンドリングをテストします。Chrome DevToolsなどのブラウザツールを使用します。

3. 手動テスト

実際にネットワークケーブルを抜く、Wi-Fiをオフにするなどして、オフライン状態や遅延を手動でテストします。

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

エラーハンドリングロジックが正しく機能することを確認するために、ユニットテストを作成します。例えば、Jestなどのテスティングフレームワークを使用してテストを行います。

const fetchWithRetry = require('./fetchWithRetry'); // 実装した関数をインポート

test('ネットワークエラー時にリトライが実行されるかを確認', () => {
    global.fetch = jest.fn()
        .mockRejectedValueOnce(new Error('ネットワークエラー'))
        .mockResolvedValueOnce({
            ok: true,
            json: () => Promise.resolve({ data: 'テストデータ' })
        });

    return fetchWithRetry('https://api.example.com/data').then(data => {
        expect(data).toEqual({ data: 'テストデータ' });
    });
});

デバッグのポイント

エラーハンドリングをデバッグする際のポイントは以下の通りです。

1. ログの活用

エラーが発生した箇所や内容をログに記録し、詳細な情報を確認できるようにします。console.errorloggingライブラリを使用します。

function handleError(error) {
    console.error('エラーログ:', error);
    // 追加のエラーログ処理
}

2. スタックトレースの確認

エラーが発生した際のスタックトレースを確認し、どの部分でエラーが発生したかを特定します。エラーオブジェクトのstackプロパティを利用します。

try {
    // エラーが発生する可能性のあるコード
} catch (error) {
    console.error('スタックトレース:', error.stack);
}

3. ブレークポイントの設定

開発者ツールを使用して、エラーハンドリングロジックにブレークポイントを設定し、ステップごとに実行状態を確認します。

4. エラーシナリオのドキュメント化

発生する可能性のあるエラーシナリオをドキュメント化し、テストケースとして管理します。これにより、全てのシナリオを漏れなくテストできるようになります。

テストとデバッグを適切に行うことで、エラーハンドリングの信頼性を高め、ユーザーにとって快適なエクスペリエンスを提供することができます。

まとめ

本記事では、JavaScriptにおけるネットワークエラーの処理方法について詳しく解説しました。ネットワークエラーの理解から始まり、Fetch APIやAxiosを使用したエラーハンドリング、ユーザーフレンドリーなエラーメッセージの表示、リトライ機能の実装、タイムアウト処理、オフライン状態の検出と処理、さらにテストとデバッグの方法まで、幅広い内容をカバーしました。

エラーハンドリングは、単にエラーをキャッチして処理するだけではなく、ユーザーに対して適切なフィードバックを提供し、アプリケーションの信頼性とユーザーエクスペリエンスを向上させるために不可欠です。適切なエラーメッセージを表示し、必要に応じてリトライやタイムアウトを設定することで、ユーザーにとって使いやすいアプリケーションを実現できます。

最後に、エラーハンドリングの実装後は、徹底したテストとデバッグを行い、さまざまなシナリオに対応できるようにすることが重要です。これにより、予期しないエラーが発生しても、アプリケーションが安定して動作し続けることが保証されます。

この記事を通じて、ネットワークエラー処理の基本から応用までの知識を習得し、実際のプロジェクトに役立てていただければ幸いです。

コメント

コメントする

目次
  1. ネットワークエラーとは何か
    1. HTTPステータスコードによるエラー
    2. ネットワーク接続の問題
    3. クライアント側のエラー
  2. ネットワークエラーの発生原因
    1. 1. サーバー側の問題
    2. 2. クライアント側の問題
    3. 3. ネットワークインフラの問題
    4. 4. ソフトウェアのバグ
  3. JavaScriptのエラーハンドリング基礎
    1. try-catch構文
    2. finallyブロック
    3. エラーオブジェクト
    4. カスタムエラーの作成
  4. Fetch APIでのエラーハンドリング
    1. 基本的なFetchの使い方
    2. HTTPステータスコードの確認
    3. ネットワークエラーの処理
    4. タイムアウトの設定
  5. Axiosを使ったエラーハンドリング
    1. Axiosの基本的な使い方
    2. HTTPステータスコードの確認
    3. ネットワークエラーの処理
    4. タイムアウトの設定
    5. リトライ機能の実装
    6. グローバルなエラーハンドリング
  6. エラーメッセージの表示方法
    1. エラーメッセージの基本原則
    2. JavaScriptでのエラーメッセージ表示例
    3. ユーザーに役立つエラーメッセージ
    4. エラーメッセージのスタイリング
  7. リトライ機能の実装
    1. 基本的なリトライロジック
    2. 指数バックオフの実装
    3. リトライ条件のカスタマイズ
  8. タイムアウト処理
    1. Fetch APIでのタイムアウト処理
    2. Axiosでのタイムアウト処理
    3. タイムアウトとリトライの組み合わせ
    4. ユーザーへのフィードバック
  9. オフライン状態の検出と処理
    1. オフライン状態の検出
    2. オフライン時のUI更新
    3. オフライン時のデータ保存と同期
  10. ユーザーフレンドリーなエラーハンドリング
    1. エラーメッセージの明確さ
    2. ユーザー行動に基づくエラー処理
    3. エラーアイコンや色の使用
    4. コンテキストに応じたエラーハンドリング
  11. テストとデバッグ
    1. ネットワークエラーのテスト手法
    2. エラーハンドリングのユニットテスト
    3. デバッグのポイント
  12. まとめ