| name | error-handling |
| description | アプリケーションのエラーハンドリング設計と実装。
例外階層、エラーレスポンス、ログ戦略、リトライ処理。
エラー処理実装、例外設計、障害対応時に使用。
|
| version | 1.0.0 |
Error Handling Skill
Overview
堅牢なエラーハンドリングパターンを提供します。
Error Hierarchy
BaseError
├── ValidationError # 入力検証エラー
├── AuthenticationError # 認証エラー
├── AuthorizationError # 認可エラー
├── NotFoundError # リソース未発見
├── ConflictError # 競合(重複など)
├── ExternalServiceError # 外部サービスエラー
│ ├── DatabaseError
│ ├── CacheError
│ └── ThirdPartyAPIError
└── InternalError # 内部エラー
Implementation
Python
from dataclasses import dataclass
from typing import Optional, List
from enum import Enum
class ErrorCode(Enum):
VALIDATION_ERROR = "VALIDATION_ERROR"
AUTHENTICATION_ERROR = "AUTHENTICATION_ERROR"
AUTHORIZATION_ERROR = "AUTHORIZATION_ERROR"
NOT_FOUND = "NOT_FOUND"
CONFLICT = "CONFLICT"
EXTERNAL_SERVICE_ERROR = "EXTERNAL_SERVICE_ERROR"
INTERNAL_ERROR = "INTERNAL_ERROR"
@dataclass
class ErrorDetail:
field: str
message: str
class AppError(Exception):
def __init__(
self,
code: ErrorCode,
message: str,
details: Optional[List[ErrorDetail]] = None,
cause: Optional[Exception] = None
):
self.code = code
self.message = message
self.details = details or []
self.cause = cause
super().__init__(message)
def to_dict(self):
return {
"error": {
"code": self.code.value,
"message": self.message,
"details": [
{"field": d.field, "message": d.message}
for d in self.details
]
}
}
class ValidationError(AppError):
def __init__(self, message: str, details: List[ErrorDetail]):
super().__init__(ErrorCode.VALIDATION_ERROR, message, details)
class NotFoundError(AppError):
def __init__(self, resource: str, identifier: str):
super().__init__(
ErrorCode.NOT_FOUND,
f"{resource} not found: {identifier}"
)
TypeScript
enum ErrorCode {
VALIDATION_ERROR = 'VALIDATION_ERROR',
AUTHENTICATION_ERROR = 'AUTHENTICATION_ERROR',
AUTHORIZATION_ERROR = 'AUTHORIZATION_ERROR',
NOT_FOUND = 'NOT_FOUND',
CONFLICT = 'CONFLICT',
EXTERNAL_SERVICE_ERROR = 'EXTERNAL_SERVICE_ERROR',
INTERNAL_ERROR = 'INTERNAL_ERROR',
}
interface ErrorDetail {
field: string;
message: string;
}
class AppError extends Error {
constructor(
public readonly code: ErrorCode,
message: string,
public readonly details: ErrorDetail[] = [],
public readonly cause?: Error
) {
super(message);
this.name = 'AppError';
}
toJSON() {
return {
error: {
code: this.code,
message: this.message,
details: this.details,
},
};
}
}
Global Error Handler (Express.js)
const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
logger.error({
error: err.message,
code: err.code,
stack: err.stack,
requestId: req.id,
path: req.path,
method: req.method,
});
if (err instanceof AppError) {
const statusCode = errorCodeToStatus(err.code);
return res.status(statusCode).json(err.toJSON());
}
return res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred',
},
});
};
function errorCodeToStatus(code: ErrorCode): number {
const mapping: Record<ErrorCode, number> = {
VALIDATION_ERROR: 400,
AUTHENTICATION_ERROR: 401,
AUTHORIZATION_ERROR: 403,
NOT_FOUND: 404,
CONFLICT: 409,
EXTERNAL_SERVICE_ERROR: 502,
INTERNAL_ERROR: 500,
};
return mapping[code] || 500;
}
Retry Strategy
interface RetryConfig {
maxRetries: number;
baseDelayMs: number;
maxDelayMs: number;
retryableErrors: ErrorCode[];
}
async function withRetry<T>(
operation: () => Promise<T>,
config: RetryConfig
): Promise<T> {
let lastError: Error;
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (!isRetryable(error, config.retryableErrors)) {
throw error;
}
if (attempt < config.maxRetries) {
const delay = Math.min(
config.baseDelayMs * Math.pow(2, attempt),
config.maxDelayMs
);
await sleep(delay + Math.random() * 100);
}
}
}
throw lastError;
}
Logging Strategy
| Level | 用途 | 例 |
|---|
| ERROR | 即時対応が必要 | 決済失敗、データ不整合 |
| WARN | 注意が必要 | リトライ発生、レート制限 |
| INFO | 重要なイベント | ユーザー登録、注文完了 |
| DEBUG | 開発用詳細 | リクエスト/レスポンス |
logger.error({
event: 'payment_failed',
userId: user.id,
orderId: order.id,
amount: order.total,
error: error.message,
errorCode: error.code,
provider: 'stripe',
requestId: context.requestId,
});