【JS応用】カスタムエラー, Error の拡張

カスタムエラーとError拡張による堅牢なエラーハンドリング戦略

フロントエンド開発において、エラーハンドリングはアプリケーションの品質を左右する極めて重要な要素です。単に「何かエラーが起きた」ことを検知するだけでなく、「何が」「なぜ」起きたのかを正確に分類し、適切なリカバリ処理やユーザーへのフィードバックに繋げる必要があります。JavaScriptの標準的なErrorオブジェクトを拡張し、アプリケーション固有のカスタムエラーを設計することは、モダンな大規模開発において必須のスキルと言えます。

標準Errorオブジェクトの限界と拡張の必要性

JavaScriptの組み込み`Error`オブジェクトは、`message`プロパティと`stack`トレースのみを持つシンプルな構造です。しかし、実際の業務アプリケーションでは、以下のような課題に直面します。

1. エラーの種類(バリデーションエラー、APIの404エラー、認証エラーなど)を`instanceof`で判別しにくい。
2. エラー発生時の文脈(HTTPステータスコード、内部的なエラーコード、再試行可能かどうかのフラグなど)を保持できない。
3. ログ収集サービスへの送信時に、エラーのメタデータを付与しにくい。

これらの課題を解決するために、`Error`クラスを継承したカスタムクラスを定義し、エラーに型とメタデータを持たせることが推奨されます。これにより、エラー処理のロジックを宣言的かつ型安全に記述することが可能になります。

カスタムエラークラスの設計と実装

カスタムエラーを実装する際、最も重要なのは「継承」と「スタックトレースの保持」です。特に、ES6以降のクラス構文を使用する場合、`Error`を継承したクラスで`super()`を呼び出すだけでは、`instanceof`が正しく機能しないケースがあります(特にトランスパイル環境において)。これを防ぐために、`Object.setPrototypeOf`を使用してプロトタイプチェーンを明示的に修正するのが定石です。

以下に、実務で使える堅牢なカスタムエラーの基本実装を示します。


class AppError extends Error {
  public readonly statusCode: number;
  public readonly isOperational: boolean;

  constructor(message: string, statusCode: number = 500, isOperational: boolean = true) {
    super(message);
    this.name = this.constructor.name;
    this.statusCode = statusCode;
    this.isOperational = isOperational;

    // Error.captureStackTraceが利用可能な環境(V8エンジン)でのスタックトレースの最適化
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, this.constructor);
    }
    
    // プロトタイプチェーンの修正(トランスパイル後の挙動安定化)
    Object.setPrototypeOf(this, new.target.prototype);
  }
}

class ValidationError extends AppError {
  constructor(message: string) {
    super(message, 400, true);
  }
}

class AuthError extends AppError {
  constructor(message: string = '認証に失敗しました') {
    super(message, 401, true);
  }
}

詳細解説:なぜこの実装が必要なのか

まず、`this.name = this.constructor.name`の指定です。デフォルトの`Error`インスタンスの`name`はすべて”Error”になりますが、これをクラス名に書き換えることで、デバッグ時やログ出力時に、どのクラスのエラーが発生したのかが即座に判別できるようになります。

次に、`isOperational`フラグの重要性です。これは「予測可能なエラー」と「予測不可能なバグ」を区別するために用います。バリデーションエラーやAPIの404エラーは、システム設計上「起こり得るエラー(Operational Error)」です。一方で、メモリ不足や未定義プロパティへのアクセスなどのプログラム上のバグは「非運用エラー(Programmer Error)」です。このフラグを見て、ユーザーにエラー内容をそのまま表示するか、あるいは「システムエラーが発生しました」という汎用メッセージに切り替えるかの判断基準になります。

また、`Error.captureStackTrace`は、エラーインスタンスが生成された場所をスタックトレースから除外(この場合はコンストラクタ自身を除外)し、より意味のある呼び出し元を表示させるためのV8エンジンの機能です。これにより、エラーの発生源を迅速に特定できるようになります。

実務での活用とエラーハンドリングのパターン

カスタムエラーを定義した後は、それを活用したエラー境界(Error Boundary)や、APIクライアントでのハンドリングを構築します。


async function fetchUserData(userId: string) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    
    if (response.status === 404) {
      throw new AppError('ユーザーが見つかりません', 404);
    }
    if (!response.ok) {
      throw new AppError('サーバーエラーが発生しました', response.status);
    }
    
    return await response.json();
  } catch (error) {
    if (error instanceof ValidationError) {
      // ユーザー入力の修正を促すUIを表示
    } else if (error instanceof AuthError) {
      // ログイン画面へリダイレクト
    } else {
      // 予期せぬエラーとしてログを送信し、汎用的なエラーモーダルを表示
      console.error('Unhandled Error:', error);
    }
    throw error; // 必要に応じて再スロー
  }
}

実務におけるアドバイスとして、エラークラスはアプリケーション全体で共有される「エラー階層」として管理することを推奨します。`api/errors.ts`のようなファイルに集約し、定数として管理することで、アプリケーションのあらゆるレイヤーから一貫した方法でエラーを生成・捕捉できます。

また、TypeScriptを使用している場合は、インターフェースを活用してエラーに詳細なメタデータ(例えば、どのフィールドでエラーが起きたかを示す`field`プロパティなど)を付与すると、フロントエンドのフォームバリデーションとの連携が劇的にスムーズになります。

まとめ:エラーを「情報」に変える

カスタムエラーを導入することは、単なるコーディングの規約ではなく、アプリケーションの「回復力(Resilience)」を高めるための重要な投資です。

1. **分類の明確化**: `instanceof`による型安全なハンドリングを実現する。
2. **コンテキストの保持**: ステータスコードやメタデータを持たせ、デバッグを容易にする。
3. **運用性の向上**: `isOperational`フラグで、ユーザーに見せるべき情報とシステム保護のための情報を峻別する。

これらを徹底することで、エラーが発生した瞬間に「何が起きたか」を即座に把握し、ユーザーに対して適切な案内を出し、開発者は修正に集中できる環境を整えることができます。エラーは隠蔽すべきものではなく、適切に構造化して扱うべき「貴重なデータ」です。ぜひ、本稿で紹介したカスタムエラーの設計手法を、自身のプロジェクトに取り入れてみてください。堅牢なエラーハンドリングは、優れたUXを実現するための隠れた基礎工事なのです。

コメント

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