【JS応用】エラーハンドリング

フロントエンド開発における堅牢なエラーハンドリング戦略:ユーザー体験を損なわないための設計指針

現代のWebアプリケーションにおいて、エラーハンドリングは単なる「例外のキャッチ」以上の意味を持ちます。複雑な状態管理、非同期データフェッチ、そして多様なネットワーク環境が絡み合うフロントエンドでは、予期せぬエラーは必ず発生します。重要なのは「エラーを発生させないこと」ではなく、「エラーが発生した際に、システムを安全に停止させ、ユーザーに適切なフィードバックを返し、開発者が迅速にデバッグできるようにすること」です。本稿では、プロフェッショナルなレベルで求められるエラーハンドリングの設計思想を詳述します。

エラーを階層化して捉える

フロントエンドにおけるエラーは、大きく3つの層に分類できます。この分類を意識することが、適切なハンドリングの第一歩です。

1. ネットワークエラー:APIの応答失敗、タイムアウト、4xx/5xx系エラー。
2. ロジックエラー:予期せぬデータ構造、型変換の失敗、状態管理の不整合。
3. UI/レンダリングエラー:Reactのコンポーネントクラッシュ、DOM操作の失敗。

これらを一括りに「try-catch」で囲むのではなく、それぞれのレイヤーに適した戦略を適用する必要があります。

Reactにおけるエラー境界(Error Boundaries)の活用

Reactアプリケーションで最も恐ろしいのは、コンポーネントツリーの一部がクラッシュし、画面全体が真っ白になる「ホワイトスクリーン」現象です。これを防ぐために必須となるのが「エラー境界」です。

エラー境界は、子コンポーネントで発生したJavaScriptエラーをキャッチし、代替のUI(フォールバックUI)を表示するコンポーネントです。注意点として、イベントハンドラ内や非同期コード(setTimeoutなど)、サーバーサイドレンダリング内のエラーはキャッチできないため、これらは個別に処理する必要があります。


import React, { Component } from 'react';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 外部ログサービス(Sentryなど)へ送信
    logErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <div>申し訳ありません。予期せぬエラーが発生しました。</div>;
    }
    return this.props.children;
  }
}

API通信における包括的なエラー処理

AxiosやFetch APIを利用する際、個別のコンポーネントで毎回エラー処理を書くのはDRY原則に反します。インターセプターを活用し、共通のエラー処理基盤を構築しましょう。

特に重要なのは、HTTPステータスコードに基づいたエラーの正規化です。401(未認証)ならログイン画面へリダイレクト、403(権限不足)ならアクセス制限画面へ、500(サーバーエラー)なら共通の通知を表示する、といったフローを中央集権的に管理します。


// apiClient.ts
import axios from 'axios';

const apiClient = axios.create({ baseURL: '/api' });

apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response) {
      switch (error.response.status) {
        case 401:
          redirectToLogin();
          break;
        case 500:
          showGlobalToast('サーバー側で問題が発生しました');
          break;
        default:
          throw new Error('予期せぬ通信エラー');
      }
    }
    return Promise.reject(error);
  }
);

型安全を活かしたエラーハンドリング

TypeScriptを使用している場合、エラーハンドリングを「型」で制御することが可能です。特に「Result型」を用いたパターンは、例外を投げる(Throw)のではなく、値を返すことでエラーを処理する手法として非常に強力です。これにより、関数がどのようなエラーを返す可能性があるのかを型定義で強制できます。


type Result<T, E> = { success: true; data: T } | { success: false; error: E };

async function fetchData(): Promise<Result<User, string>> {
  try {
    const data = await api.get('/user');
    return { success: true, data };
  } catch (e) {
    return { success: false, error: '取得失敗' };
  }
}

// 利用側
const result = await fetchData();
if (!result.success) {
  console.error(result.error);
  return;
}
console.log(result.data);

実務におけるアドバイス:観測可能性(Observability)の確保

開発環境でエラーに気づくことは容易ですが、本番環境でユーザーが遭遇しているエラーを把握することは困難です。以下の3点を実務の指針としてください。

1. Sentry等のエラー監視ツールの導入:
ブラウザで発生した未補足の例外をリアルタイムで収集し、スタックトレースとユーザーのコンテキスト(OS、ブラウザ、操作履歴)を記録してください。

2. ユーザーへの「誠実な」メッセージング:
「不明なエラーです」だけで済ませず、「現在システムが混雑しています。数分後に再度お試しください」といった、具体的なアクションを促すメッセージを表示しましょう。また、サポートへ問い合わせるためのエラーIDを生成して表示することも有効です。

3. エラーの分類とログレベル:
すべてのエラーを重大なものとして扱うと、通知が溢れて重要な問題を見逃します。「警告(Warning)」レベルと「致命的(Fatal)」レベルを明確に分け、監視ツールでフィルタリングできるようにしてください。

まとめ

フロントエンドにおけるエラーハンドリングは、単なるバグ潰しではなく、プロダクトの信頼性を支える重要なインフラです。Reactのエラー境界によるUI保護、インターセプターによるAPI通信の集中管理、そしてResult型による型安全なエラー処理を組み合わせることで、極めて堅牢なアプリケーションを実現できます。

完璧なソフトウェアは存在しませんが、エラーが発生した際の「振る舞い」を設計しておくことは可能です。ユーザーが迷子にならず、開発者が最短で原因を特定できる環境を作ること。それこそが、シニアエンジニアとして求められるエラーハンドリングの極意です。コードを書く際は、常に「もしここでエラーが起きたら?」という問いを自分自身に投げかけ、その答えを実装に落とし込んでください。

コメント

タイトルとURLをコピーしました