error-handling-patterns について
TypeScript、Python、React でのエラー処理戦略を選び、適用するためのソフトウェア開発スキルです。エラーの意味に応じた扱い方を示し、型付きエラー、再試行、復旧、診断情報を扱います。
主な機能
- 01例外、Result 型、エラーコードの比較
- 02一時的エラーと永続的エラーの分類
- 03型付きエラー階層の設計
- 04再試行、バックオフ、サーキットブレーカーの適用
- 05React のレンダリングエラー処理
- 06Python の例外チェーンの保持
活用シーン
- 01ライブラリや CLI のエラー形式の選択
- 02一時的な障害に対する再試行の設計
- 03ルート単位の React エラーバウンダリーの追加
- 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.jsonreferences/retry-patterns.mderror-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 モナドの実装、エラーバウンダリーパターンについて参照してください。