TypeScript 速習チュートリアル

TypeScript エラーハンドリング

信頼性の高い TypeScript アプリケーションを構築する上で、堅牢なエラーハンドリングは極めて重要です。
本ガイドでは、基本的な try/catch から、高度なエラーハンドリングパターンまでを網羅的に解説します。

1. エラーハンドリングの基礎 (Try/Catch ブロック)

エラーハンドリングの最も基本的な構成要素は以下の通りです。

コード例

function divide(a: number, b: number): number {
  if (b === 0) {
    throw new Error('0で割ることはできません');
  }
  return a / b;
}

try {
  const result = divide(10, 0);
  console.log(result);
} catch (error) {
  // エラーメッセージの出力
  console.error('エラーが発生しました:', error.message);
}

1.1 TypeScript 4.0 以降の注意点

TypeScript 4.0 以降では、catch ブロックの変数はデフォルトで unknown 型になります。プロパティにアクセスする前に、必ず型を絞り込む(Narrowing)必要があります。

2. カスタムエラークラス (Custom Error Classes)

2.1 カスタムエラークラスの作成

組み込みの Error クラスを継承して、ドメイン固有のエラーを作成します。

コード例

class ValidationError extends Error {
  constructor(message: string, public field?: string) {
    super(message);
    this.name = 'ValidationError';
    // プロトタイプチェーンの復元(TSで重要)
    Object.setPrototypeOf(this, ValidationError.prototype);
  }
}

class DatabaseError extends Error {
  constructor(message: string, public code: number) {
    super(message);
    this.name = 'DatabaseError';
    Object.setPrototypeOf(this, DatabaseError.prototype);
  }
}

// 使用例
function validateUser(user: any) {
  if (!user.name) {
    throw new ValidationError('名前は必須です', 'name');
  }
  if (!user.email.includes('@')) {
    throw new ValidationError('メールアドレスの形式が正しくありません', 'email');
  }
}

3. エラーのための型ガード (Type Guards for Errors)

3.1 エラーハンドリングのための型述語

異なるエラー型を安全に扱うために、型ガード(Type Guards)を作成します。

コード例

// 型述語(Type Predicates)
function isErrorWithMessage(error: unknown): error is { message: string } {
  return (
    typeof error === 'object' &&
    error !== null &&
    'message' in error &&
    typeof (error as Record<string, unknown>).message === 'string'
  );
}

function isValidationError(error: unknown): error is ValidationError {
  return error instanceof ValidationError;
}

// catch ブロックでの使用
try {
  validateUser({});
} catch (error: unknown) {
  if (isValidationError(error)) {
    console.error(`バリデーションエラー(フィールド: ${error.field}): ${error.message}`);
  } else if (isErrorWithMessage(error)) {
    console.error('エラーが発生しました:', error.message);
  } else {
    console.error('不明なエラーが発生しました');
  }
}

3.2 型アサーションパターン

より複雑なエラーハンドリングでは、型アサーション関数を使用することを検討してください。

コード例

function assertIsError(error: unknown): asserts error is Error {
  if (!(error instanceof Error)) {
    throw new Error('キャッチされた値は Error インスタンスではありません');
  }
}

try {
  // ... 処理
} catch (error) {
  assertIsError(error);
  // TypeScript はここで error が Error インスタンスであることを認識します
  console.error((error as Error).message);
}

4. 非同期エラーハンドリング (Async Error Handling)

4.1 Async/Await エラーのハンドリング

async/await コードにおける適切なエラーハンドリングには、await 呼び出しを try/catch ブロックでラップする必要があります。

コード例

interface User {
  id: number;
  name: string;
  email: string;
}

// async/await と try/catch の併用
async function fetchUser(userId: number): Promise<User> {
  try {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) {
      throw new Error(`HTTPエラー! ステータス: ${response.status}`);
    }
    return await response.json() as User;
  } catch (error) {
    if (error instanceof Error) {
      console.error('ユーザーの取得に失敗しました:', error.message);
    }
    throw error; // 呼び出し元が処理できるように再スロー
  }
}

// Promise.catch() を使用したエラーハンドリング
function fetchUserPosts(userId: number): Promise<any[]> {
  return fetch(`/api/users/${userId}/posts`)
    .then(response => {
      if (!response.ok) {
        throw new Error(`HTTPエラー! ステータス: ${response.status}`);
      }
      return response.json();
    })
    .catch(error => {
      console.error('投稿の取得に失敗しました:', error);
      return []; // フォールバックとして空配列を返す
    });
}

4.2 未処理の Promise リジェクション

警告を避けるため、必ず Promise のリジェクション(拒否)を処理してください。

コード例

// NG: 未処理の Promise リジェクション
fetchData().then(data => console.log(data));

// OK: 成功とエラーの両方を処理
fetchData()
  .then(data => console.log('成功:', data))
  .catch(error => console.error('エラー:', error));

// または、意図的にエラーを無視する場合は void を使用
void fetchData().catch(console.error);

5. React における Error Boundary

5.1 React Error Boundary コンポーネント

React のコンポーネントツリー内で JavaScript エラーをキャッチするための Error Boundary(エラー境界)を作成します。

コード例

import React, { Component, ErrorInfo, ReactNode } from 'react';

interface ErrorBoundaryProps {
  children: ReactNode;
  fallback?: ReactNode;
}

interface ErrorBoundaryState {
  hasError: boolean;
  error?: Error;
}

class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
  public state: ErrorBoundaryState = {
    hasError: false
  };

  public static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error };
  }

  public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('キャッチされなかったエラー:', error, errorInfo);
    // エラーレポートサービスへのログ出力など
  }

  public render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div className="error-boundary">
          <h2>問題が発生しました</h2>
          <p>{this.state.error?.message}</p>
          <button onClick={() => this.setState({ hasError: false })}>
            再試行する
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// 使用例
function App() {
  return (
    <ErrorBoundary fallback={<div>おっと!何かが壊れました。</div>}>
      <MyComponent />
    </ErrorBoundary>
  );
}

6. ベストプラクティス (Best Practices)

6.1 必ずエラーを処理する

catch ブロックを空のままにしないでください。最低限、エラーをログに記録しましょう。

コード例

// NG: サイレントエラー(黙殺)
try { /* ... */ } catch { /* 空 */ }

// OK: 少なくともエラーをログ出力する
try { /* ... */ } catch (error) {
  console.error('操作に失敗しました:', error);
}

6.2 具体的なエラー型を使用する

異なるエラーシナリオに合わせて、カスタムエラークラスを作成します。

コード例

class NetworkError extends Error {
  constructor(public status: number, message: string) {
    super(message);
    this.name = 'NetworkError';
  }
}

class ValidationError extends Error {
  constructor(public field: string, message: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

6.3 適切なレベルでエラーを処理する

回復(リカバリ)が可能であったり、ユーザーに適切なフィードバックを提供できるコンテキストを持っている場所でエラーを処理します。

コード例

// データアクセスレイヤー
async function getUser(id: string): Promise<any> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new NetworkError(response.status, 'ユーザーの取得に失敗しました');
  }
  return response.json();
}

// UIコンポーネント
async function loadUser() {
  try {
    const user = await getUser('123');
    setUser(user);
  } catch (error) {
    if (error instanceof NetworkError) {
      if (error.status === 404) {
        showError('ユーザーが見つかりません');
      } else {
        showError('ネットワークエラーが発生しました。後でもう一度お試しください。');
      }
    } else {
      showError('予期せぬエラーが発生しました');
    }
  }
}

7. よくある落とし穴 (Common Pitfalls)

7.1 Promise のリジェクションを無視する

未処理のリジェクション警告を防ぐため、必ず Promise の失敗をハンドリングしてください。

7.2 型の絞り込みをせずに catch する

TypeScript 4.0 以降、キャッチされたエラーは unknown 型になります。

コード例

// NG: エラーが 'unknown' 型
try { /* ... */ } catch (error) {
  console.log(error.message); // エラー: 'unknown' 型にプロパティ 'message' は存在しません
}

// OK: 型を絞り込む
try { /* ... */ } catch (error) {
  if (error instanceof Error) {
    console.log(error.message); // OK
  }
}

7.3 エラーの飲み込み(Swallowing Errors)

適切な処理を行わずに、エラーを黙ってキャッチして無視することは避けてください。

コード例

// NG: エラーがサイレントに無視される
function saveData(data: Data) {
  try {
    database.save(data);
  } catch {
    // 無視
  }
}

// より良い方法: エラーをログに記録し、ユーザーに通知する
function saveData(data: Data) {
  try {
    database.save(data);
  } catch (error) {
    console.error('データの保存に失敗しました:', error);
    showError('データの保存に失敗しました。もう一度お試しください。');
  }
}

8. まとめ (Summary)

TypeScript における効果的なエラーハンドリングのポイントは以下の通りです:

  • 同期コードには try/catch ブロックを使用する
  • 非同期コードには .catch() または async/await + try/catch を使用する
  • ドメイン固有のエラーには カスタムエラークラス を作成する
  • 型ガード を使用して、エラーオブジェクトを安全に操作する
  • アプリケーション内の 適切な階層 でエラーを処理する
  • ユーザーに 意味のあるエラーメッセージ を提供する

これらのプラクティスに従うことで、より堅牢でメンテナンス性の高い TypeScript アプリケーションを構築することができます。