mcpField
E
error-handling-patterns

error-handling-patterns

github.com/curiositech開発263132 回インストール2 日前に更新
リポジトリ

error-handling-patterns について

TypeScript、Python、React でのエラー処理戦略を選び、適用するためのソフトウェア開発スキルです。エラーの意味に応じた扱い方を示し、型付きエラー、再試行、復旧、診断情報を扱います。

主な機能

  1. 01例外、Result 型、エラーコードの比較
  2. 02一時的エラーと永続的エラーの分類
  3. 03型付きエラー階層の設計
  4. 04再試行、バックオフ、サーキットブレーカーの適用
  5. 05React のレンダリングエラー処理
  6. 06Python の例外チェーンの保持

活用シーン

  1. 01ライブラリや CLI のエラー形式の選択
  2. 02一時的な障害に対する再試行の設計
  3. 03ルート単位の React エラーバウンダリーの追加
  4. 04Python で例外を再送出する際のコンテキスト保持

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/curiositech/some_claude_skills --skill error-handling-patterns

インストール後、Claude Code で/error-handling-patternsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

.claude-plugin/
plugin.json
references/
retry-patterns.md
error-hierarchy-examples.md

詳細

エラー処理のパターン

失敗を明確にし、復旧可能で、デバッグしやすいエラー処理戦略を設計します。中心となるスキルは、エラーの意味に合わせてエラー処理のスタイルを選ぶことです。すべてのエラーが同じではなく、同じように扱うと、どのエラーにも同じようにうまく対処できないシステムになります。

使用する場面

✅ 次のような場合に使用します:

  • ドメインに適した例外、Result 型、エラーコードの選択
  • TypeScript または Python での型付きエラー階層の設計
  • バックオフ、ジッター、サーキットブレーカーを使った再試行ロジックの実装
  • React のエラーバウンダリーとグレースフルデグラデーションの構築
  • ユーザーと開発者の両方に向けたエラー情報の構成
  • Python の例外チェーンと __cause__ / __context__ のセマンティクス

❌ 次のような場合には使用しません:

  • 特定の実行時エラーのデバッグ(デバッガーまたはドメインスキルを使用してください)
  • ロギングパイプラインのインフラストラクチャ(オブザーバビリティスキルを使用してください)
  • APM/監視の設定(site-reliability-engineer スキルを使用してください)
  • エラーパスのテスト作成(vitest-testing-patterns スキルを使用してください)

主な判断事項:例外、Result 型、エラーコードのどれを使うか

flowchart TD
    Q1{Is this a programming error\nor contract violation?} -->|Yes| EX[Throw exception\nlet it crash]
    Q1 -->|No| Q2{Is the error part of\nnormal control flow?}
    Q2 -->|Yes| Q3{What is the call site context?}
    Q2 -->|No| Q4{Do callers need to\ndistinguish error types?}
    Q3 -->|Functional / monad-friendly| RT[Result or Either type]
    Q3 -->|Simple script or CLI| EC[Error code + message]
    Q4 -->|Yes| EH[Typed exception hierarchy]
    Q4 -->|No| GE[Generic exception\nwith structured message]
    EX --> NOTE1[Never catch at boundary —\nlet process restart]
    RT --> NOTE2[Compose with map/flatMap;\ncheck references/error-hierarchy-examples.md]
    EH --> NOTE3[See hierarchy design rules below]

経験則:

  • ライブラリコード:Result 型を優先します。呼び出し側に例外の処理を決して強制しないでください
  • アプリケーションコード:型付き例外階層が適しています。エラーは例外的なものです
  • CLI /スクリプト:エラーコードで問題ありません。ユーザーがエラー境界になります
  • 非同期ワーカー:Result 型、または再試行メタデータを含む構造化エラーオブジェクトを使用します

エラーの分類

エラーへの対処方法を決める前に、すべてのエラーを次の 2 つの軸で分類します:

一時的(再試行で成功する可能性がある)恒久的(再試行しても解決しない)
ユーザーが対処可能レート制限、クォータ超過無効な入力、認証されていない
システムが対処可能ネットワークタイムアウト、DB 接続データ破損、スキーマ不一致

この分類によって、次のことが決まります:

  • 再試行するかどうか(一時的な場合のみ)
  • ユーザーに何を表示するか(ユーザーが対処可能 → メッセージ、システム → 一般的なエラーと追跡 ID)
  • オンコールにアラートを送るかどうか(システムの恒久的なエラー → ページ通知、一時的なエラーの急増 → アラート)

このエラーは再試行するべきですか?

flowchart TD
    E[Error occurs] --> C1{Is error transient?\nTimeout, 429, 503, connection reset}
    C1 -->|No| FAIL[Fail immediately\nReturn error to caller]
    C1 -->|Yes| C2{Have we exceeded\nmax retry attempts?}
    C2 -->|Yes| DLQ[Send to dead letter queue\nor return final failure]
    C2 -->|No| C3{Is circuit breaker OPEN?}
    C3 -->|Yes| CB[Return circuit-open error\nDo not attempt request]
    C3 -->|No| WAIT[Wait: exponential backoff\n+ full jitter]
    WAIT --> RETRY[Retry request]
    RETRY --> C1
    CB --> PROBE{After timeout:\nsend probe request}
    PROBE -->|Success| CLOSE[Close circuit\nResume normal traffic]
    PROBE -->|Fail| CB

バックオフの計算式、ジッター戦略、サーキットブレーカーの実装については、references/retry-patterns.md を参照してください。


TypeScript:エラー階層の設計

// Base application error — all domain errors extend this
class AppError extends Error {
  readonly code: string;
  readonly statusCode: number;
  readonly isOperational: boolean; // false = programmer error, crash process

  constructor(message: string, code: string, statusCode: number, isOperational = true) {
    super(message);
    this.name = this.constructor.name;
    this.code = code;
    this.statusCode = statusCode;
    this.isOperational = isOperational;
    Error.captureStackTrace(this, this.constructor);
  }
}

// Domain-specific errors
class ValidationError extends AppError {
  readonly fields: Record<string, string[]>;
  constructor(fields: Record<string, string[]>) {
    super('Validation failed', 'VALIDATION_ERROR', 422);
    this.fields = fields;
  }
}

class NotFoundError extends AppError {
  constructor(resource: string, id: string) {
    super(`${resource} ${id} not found`, 'NOT_FOUND', 404);
  }
}

class RateLimitError extends AppError {
  readonly retryAfterMs: number;
  constructor(retryAfterMs: number) {
    super('Rate limit exceeded', 'RATE_LIMIT', 429);
    this.retryAfterMs = retryAfterMs;
  }
}

Python での同等の例、Result 型の実装、完全な階層パターンについては、references/error-hierarchy-examples.md を参照してください。


Result 型パターン(TypeScript)

エラーが操作(解析、API 呼び出し、DB クエリ)の想定される結果である場合は、throw の代わりに Result を使用します:

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

// Helpers
const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
const err = <E>(error: E): Result<never, E> => ({ ok: false, error });

// Usage — caller is forced to handle both cases
async function fetchUser(id: string): Promise<Result<User, NotFoundError | NetworkError>> {
  try {
    const user = await db.users.findById(id);
    if (!user) return err(new NotFoundError('User', id));
    return ok(user);
  } catch (e) {
    return err(new NetworkError('DB unavailable', { cause: e }));
  }
}

// At call site — no silent failures
const result = await fetchUser(userId);
if (!result.ok) {
  if (result.error instanceof NotFoundError) return res.status(404).json(...);
  return res.status(500).json(...);
}
const user = result.value; // typed, safe

React のエラーバウンダリー

エラーバウンダリーはレンダー時の例外を捕捉します。非同期エラー(fetch の失敗、setTimeout、イベントハンドラー)は捕捉しません。

class RouteErrorBoundary extends React.Component<Props, State> {
  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    // Log to error tracking, not console.error in production
    logger.error('Render error', { error, componentStack: info.componentStack });
  }

  render() {
    if (this.state.hasError) {
      return <ErrorFallback error={this.state.error} onRetry={this.reset} />;
    }
    return this.props.children;
  }
}

ルートレベル(ページごとに 1 つ)と、独立した負荷の高いサブツリー(チャート、リッチエディター)の周囲にバウンダリーを配置します。すべてのコンポーネントを囲まないでください。細かく分けすぎると効果が損なわれます。


Python:例外チェーン

Python の raise X from Y 構文は因果関係のチェーンを保持します。再送出する際は、必ず使用してください:

class AppError(Exception):
    """Base error. All domain errors subclass this."""
    def __init__(self, message: str, code: str, status: int = 500):
        super().__init__(message)
        self.code = code
        self.status = status

class DatabaseError(AppError):
    def __init__(self, operation: str, cause: Exception):
        super().__init__(f"DB error during {operation}", "DB_ERROR", 503)
        self.__cause__ = cause  # explicit chain

# In application code
try:
    result = db.execute(query)
except psycopg2.OperationalError as e:
    raise DatabaseError("user_fetch", e) from e  # preserves full traceback

構造化エラーロギング

コードを読まなくても原因を診断できるだけのコンテキストを含めて、エラーを記録します:

// Good: structured, queryable, developer-oriented
logger.error('Payment processing failed', {
  error: {
    code: error.code,
    message: error.message,
    stack: error.stack,
  },
  context: {
    userId,
    orderId,
    amount,
    paymentProvider,
    attempt: retryCount,
  },
  correlation: { requestId, traceId },
});

// Then surface a sanitized message to the user
// NEVER leak error.message to users — it may contain internals
return res.status(500).json({
  error: 'Payment could not be processed. Please try again.',
  errorId: requestId, // so support can look it up
});

アンチパターン

アンチパターン:ポケモン式例外処理

初心者:「すべてを try/catch で囲んで、エラーをログに記録すればいい。少なくともクラッシュはしない。」

エキスパート:すべての例外を無条件に捕捉すること(「すべて捕まえよう」)は、プログラマーのミスを隠し、リソースリークを覆い隠し、表面化していた失敗を気づかれないデータ破損に変えてしまいます。データがひそかに破棄されているのに、システムは健全に見えてしまいます。

// Wrong — swallows everything including programming errors
try {
  await processOrder(order);
} catch (e) {
  console.error('something went wrong', e); // lost forever
}

// Right — catch only what you can handle, let the rest propagate
try {
  await processOrder(order);
} catch (e) {
  if (e instanceof RateLimitError) {
    await queue.requeue(order, { delay: e.retryAfterMs });
    return;
  }
  // programming errors, unexpected DB errors — let them crash
  throw e;
}

検出方法:catch (e) { }、再送出を行わない catch (e) { log(e) }、Python の except Exception as e: pass。条件分岐も再送出もない catch ブロックはすべて該当します。

経緯:これは昔から誤りです。async/await の時代(2017 年以降)に改めて重要になりました。握りつぶされた Promise の拒否は、握りつぶされた同期例外よりも検出が難しいためです。


反パターン:文字列型のエラー

初心者:「エラーメッセージ文字列にエラーの種類を入れます。throw new Error('NOT_FOUND: User 123')」

エキスパート:文字列ベースのエラー型では、呼び出し側が文字列を解析する必要があり、リファクタリングで壊れやすく、IDE のサポートも得られず、網羅的なマッチングもできません。呼び出し側は、コードベースの進化に伴って変わっていく文字列を使ってパターンマッチングします。

// Wrong — caller must parse strings, breaks silently on rename
throw new Error(`RATE_LIMIT: retry after ${ms}ms`);
// Caller: if (error.message.startsWith('RATE_LIMIT')) { ... }

// Right — typed, refactor-safe, IDE-navigable
throw new RateLimitError(ms);
// Caller: if (error instanceof RateLimitError) { ... error.retryAfterMs ... }

Python での同等の例:

# Wrong
raise Exception(f"rate_limit:{retry_after}")

# Right
raise RateLimitError(retry_after_ms=retry_after)

LLM によくある誤り:StackOverflow の例で学習した LLM は、SO の回答が正確さより簡潔さを優先するため、文字列型のエラーを頻繁に生成します。チュートリアルでは、エラーコードを文字列にすると簡潔に見えます。

検出方法:instanceof Error チェックがいたるところにある、catch ブロック内で文字列の .startsWith() や .includes() を使っている、専用のプロパティではなく message フィールドにエラーコードが保存されている。


参考資料

  • references/retry-patterns.md — リトライロジックを実装する際に参照してください。指数バックオフの計算式、フルジッターと均等ジッターの比較、サーキットブレーカーの状態遷移、デッドレターキューについて説明しています。
  • references/error-hierarchy-examples.md — TypeScript と Python の型付きエラークラスの完全な例、Result モナドの実装、エラーバウンダリーパターンについて参照してください。

コメント