LESSON 15分

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

ストーリー

「本番環境で一番怖いのは何だと思う?」

「バグですか?」

「バグは直せる。一番怖いのはエラーを握りつぶすことだ。 エラーが起きたのに何も表示されず、何も記録されず、 気づいた時にはデータが壊れている... そんな事故は後を絶たない」


エラーの基本: try/catch/finally

typescript
try {
  // エラーが起きるかもしれない処理
  const data = JSON.parse(jsonString);
  processData(data);
} catch (error) {
  // エラーが起きた時の処理
  if (error instanceof SyntaxError) {
    console.error("JSONの形式が不正です:", error.message);
  } else if (error instanceof Error) {
    console.error("予期しないエラー:", error.message);
  }
} finally {
  // 必ず実行される(成功・失敗に関係なく)
  cleanup();
}

カスタムエラークラス

typescript
// 独自のエラークラスを定義
class ValidationError extends Error {
  constructor(
    message: string,
    public readonly field: string
  ) {
    super(message);
    this.name = "ValidationError";
  }
}

class NotFoundError extends Error {
  constructor(
    public readonly resource: string,
    public readonly id: string | number
  ) {
    super(`${resource} (ID: ${id}) が見つかりません`);
    this.name = "NotFoundError";
  }
}

class ApiError extends Error {
  constructor(
    message: string,
    public readonly statusCode: number
  ) {
    super(message);
    this.name = "ApiError";
  }
}

// 使用例
function findUser(id: number): User {
  const user = users.find((u) => u.id === id);
  if (!user) {
    throw new NotFoundError("User", id);
  }
  return user;
}

// エラーの種類に応じた処理
try {
  const user = findUser(999);
} catch (error) {
  if (error instanceof NotFoundError) {
    console.log(`${error.resource}が見つかりません`);
  } else if (error instanceof ValidationError) {
    console.log(`入力エラー: ${error.field} - ${error.message}`);
  } else {
    throw error; // 未知のエラーは再スロー
  }
}

エラーハンドリングのルール

1. エラーを握りつぶさない

typescript
// 絶対にやってはいけない
try {
  riskyOperation();
} catch (error) {
  // 何もしない ← 最悪のパターン
}

// 最低限ログは残す
try {
  riskyOperation();
} catch (error) {
  console.error("操作に失敗しました:", error);
  // 必要に応じてエラーを再スロー
  throw error;
}

2. catch (error) の型チェック

typescript
// TypeScript では catch の error は unknown 型
try {
  someOperation();
} catch (error) {
  // 良い例: 型チェックしてからアクセス
  if (error instanceof Error) {
    console.error(error.message);
    console.error(error.stack);
  } else {
    console.error("不明なエラー:", error);
  }
}

3. エラーは適切なレベルで処理する

typescript
// 低レベル: エラーをスローするだけ
function parseConfig(json: string): Config {
  try {
    return JSON.parse(json);
  } catch {
    throw new ValidationError("設定ファイルのJSON形式が不正です", "config");
  }
}

// 高レベル: エラーをキャッチしてユーザーに通知
async function loadApp(): Promise<void> {
  try {
    const config = parseConfig(rawConfig);
    await startServer(config);
  } catch (error) {
    if (error instanceof ValidationError) {
      console.error(`設定エラー: ${error.message}`);
      console.error("設定ファイルを確認してください");
      process.exit(1);
    }
    throw error;
  }
}

4. 非同期処理のエラーハンドリング

typescript
// async/await では try/catch を使う
async function fetchUser(id: number): Promise<User> {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      throw new ApiError(`ユーザー取得失敗`, response.status);
    }
    return await response.json();
  } catch (error) {
    if (error instanceof ApiError) {
      throw error;
    }
    throw new ApiError("ネットワークエラー", 0);
  }
}

エラーハンドリングパターン

Result型パターン(例外を使わない方法)

typescript
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function divide(a: number, b: number): Result<number> {
  if (b === 0) {
    return { ok: false, error: new Error("0で割ることはできません") };
  }
  return { ok: true, value: a / b };
}

const result = divide(10, 0);
if (result.ok) {
  console.log(`結果: ${result.value}`);
} else {
  console.error(`エラー: ${result.error.message}`);
}

まとめ

ポイント内容
try/catchエラーをキャッチして適切に処理する
カスタムエラーError を継承して独自エラーを定義
握りつぶし禁止catch で何もしないのは絶対NG
型チェックinstanceof でエラーの種類を判定
適切なレベル低レベルはスロー、高レベルでキャッチ

チェックリスト

  • try/catch/finally を適切に使える
  • カスタムエラークラスを作れる
  • エラーを握りつぶしてはいけない理由を理解した
  • instanceof でエラーの種類を判定できる

次のステップへ

エラーハンドリングの基本を学びました。

次のセクションでは、ESLint/Prettierを使ってコード品質を自動的に守る方法を学びます。


推定読了時間: 15分