C
TypeScript/非同期/Lesson 06

エラー処理 — catch の err は unknown。必ず絞り込んで使え

30分·theory
このチャプター
6/7
TypeScript

エラー処理 — catch の err は unknown。必ず絞り込んで使え

💡 なぜ学ぶ必要があるのか — キャッチしても終わりではない

🎯 JSの`catch`では`err`が`any`型のため、`.message`に何の確認もなくアクセスできます。TSは`unknown`を強制することで「本当にErrorかどうか」を確認させます。
💼 `throw`は何でも投げられます — Error、文字列、数値、オブジェクト。`catch`内で形状を決め打ちするのは危険です。
カスタムErrorクラス(ApiError、ValidationErrorなど)でエラーを分類することで、呼び出し元がそれぞれ異なる処理を行えます。
🔗 `try/finally`でリソースのクリーンアップ(ファイルのクローズ・ロックの解放)を保証します — エラーの有無にかかわらず実行されます。
🏢 실무에서는
APIコールの失敗には401/403/404/500それぞれ異なるUXが必要です。401 → ログインページへリダイレクト、403 → 権限の案内、404 → 空の状態を表示、500 → 一時的なエラーメッセージを表示。`catch`で`err.status === 401`のように分岐するには、エラーオブジェクトの型を知る必要があります — TypeScriptの`instanceof ApiError`がその安全な入り口です。

unknown の絞り込み · カスタム Error · try/finally

1. catch (err) の err は unknown (TS 4.4+)

ts
try { riskyOp(); }
catch (err) {
  // err は unknown — メソッドもプロパティも一切アクセス不可
  // err.message; ❌ Object is of type 'unknown'
}

2. 絞り込みの3パターン

ts
catch (err) {
  // (a) instanceof — クラスベース
  if (err instanceof Error) {
    console.log(err.message); // ✅ Error に絞り込まれた
  }

  // (b) ユーザー定義型ガード
  if (isApiError(err)) {
    console.log(err.status); // ✅
  }

  // (c) typeof — プリミティブ型
  if (typeof err === 'string') {
    console.log(err.toUpperCase());
  }
}

function isApiError(e: unknown): e is ApiError {
  return e instanceof Error && 'status' in e;
}

3. カスタム Error クラス — エラーを分類する

ts
class ApiError extends Error {
  constructor(message: string, public status: number) {
    super(message);
    this.name = 'ApiError'; // スタックトレース・ログ用
  }
}

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

throw new ApiError('認証失敗', 401);
throw new ValidationError('メール形式が不正', 'email');

呼び出し側では:

ts
catch (err) {
  if (err instanceof ApiError && err.status === 401) {
    redirectToLogin();
  } else if (err instanceof ValidationError) {
    showFieldError(err.field, err.message);
  } else if (err instanceof Error) {
    showGenericError(err.message);
  }
}

4. try/finally — クリーンアップは必ず実行される

ts
const conn = await db.connect();
try {
  await conn.query('SELECT ...');
} catch (err) {
  console.log('クエリ失敗');
} finally {
  await conn.close(); // エラーが発生しても、しなくても、return で抜けても実行される
}
💻 🅰️ JS の流儀 — catch で .message に直接アクセスする
// ❌ JS — err が何であるか分からないまま使用

async function riskyOp() {
  if (Math.random() < 0.5) throw new Error('実際のエラー');
  else throw '文字列エラー'; // 誰かがこのように投げると
}

async function main() {
  try {
    await riskyOp();
  } catch (err) {
    console.log(err.message);  // 運が良ければ OK、文字列なら undefined
    console.log(err.status);   // 常に undefined — 不明
  }
}
main();

// エラーの種類を分岐しようとしても
try { await riskyOp(); }
catch (err) {
  if (err.code === 'AUTH') redirectLogin();   // err.code が存在するか保証なし
  else if (err.status === 401) redirectLogin(); // err.status が存在するか保証なし
  else console.log(err);
}
💻 🅱️ TS の流儀 — unknown の絞り込み + カスタム Error
// ✅ TS — カスタムエラー + unknown の絞り込み

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

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

async function riskyOp(): Promise<void> {
  const r = Math.random();
  if (r < 0.33) throw new ApiError('認証失敗', 401);
  if (r < 0.66) throw new ValidationError('メール形式', 'email');
  throw new Error('不明なエラー');
}

async function main(): Promise<void> {
  try {
    await riskyOp();
  } catch (err) {
    // err は unknown — そのまま .message にアクセス不可
    if (err instanceof ApiError) {
      if (err.status === 401) console.log('ログインページへ');
      else console.log(`API ${err.status}: ${err.message}`);
    } else if (err instanceof ValidationError) {
      console.log(`フィールド [${err.field}]: ${err.message}`);
    } else if (err instanceof Error) {
      console.log('一般:', err.message);
    } else {
      console.log('不明な形式の throw:', err);
    }
  } finally {
    console.log('クリーンアップ作業 — 常に実行');
  }
}
main();

// ユーザー定義型ガードパターン
function isApiError(e: unknown): e is ApiError {
  return e instanceof ApiError;
}
// 呼び出し元で可読性のために isApiError(err) のように使える

💡 💡 TypeScript エラー処理の重要ポイント5選

1. catch の err は unknown — 絞り込め

ts
if (err instanceof Error) console.log(err.message);

2. カスタム Error はクラスで作り、name を必ず設定せよ

ts
class ApiError extends Error {
  constructor(msg: string, public status: number) {
    super(msg);
    this.name = 'ApiError'; // ログ・スタックに表示される
  }
}

3. エラーの分類には instanceof を使え
err.code === 'XYZ' のような文字列比較はタイポや重複のリスクがある。クラスベースはコンパイル時に安全。

4. finally は必ず実行される — リソースのクリーンアップに使え
ファイルのクローズ・データベース接続の解放・ロックの解除などは finally で行う。

5. 空の catch は絶対に書くな

ts
try { ... } catch {} // 🚨 エラーを握り潰す — デバッグ地獄

最低でも console.error(err) を入れること。さもなければ再スローする。

⚡ 実際に試してみよう — カスタム Error + instanceof

種類ごとにエラーを throw し、catch 内で分岐処理します。
✏️ JS 코드
📟 コンソール出力
▶ 実行ボタンを押してください
⚠️ ブラウザのサンドボックスで実行 — console.log()のみ対応、alert/fetchは不可

理解度チェッククイズ

TypeScript で `catch (err) { ... }` の `err` を最も安全に絞り込む方法は何ですか?
💡 `throw`は何でも投げられるため(Error、文字列、オブジェクト)、`err`の型は`unknown`です。安全な入り口は`instanceof Error`による絞り込みです。キャスト(`as Error`)や`any`アクセスはコンパイルは通りますが、実行時の安全性を失います。
エラー処理 — catch の unknown を絞り込む - TypeScript