エラーハンドリングパターン - 堅牢なアプリケーション設計

15分 で読める | 2025.01.10

公式ドキュメント

エラーハンドリングの原則

エラーハンドリングの3原則:

  1. 早期に失敗する(Fail Fast) - 問題を早期に検出
  2. 明示的にする - 暗黙のエラーを避ける
  3. 回復可能性を考慮 - リトライ可能かを判断
  4. ユーザーに適切なフィードバック - 技術的詳細は隠す

エラーの分類

種類対処法
予期されるエラー入力バリデーション失敗ユーザーにフィードバック
回復可能なエラーネットワークタイムアウトリトライ
致命的エラーメモリ不足ログ出力して終了
プログラムエラーnull参照バグ修正

ポイント: Result型パターンを使うと、エラーが戻り値の型に含まれるためエラー処理の漏れをコンパイル時に検出できます

Result型パターン

例外を使わず、戻り値でエラーを表現します。

type Result<T, E = Error> =
 | { success: true; data: T }
 | { success: false; error: E };

// 使用例
async function fetchUser(id: string): Promise<Result<User, ApiError>> {
 try {
 const response = await fetch(`/api/users/${id}`);

 if (!response.ok) {
 return {
 success: false,
 error: new ApiError(response.status, 'User not found'),
 };
 }

 const user = await response.json();
 return { success: true, data: user };

 } catch (error) {
 return {
 success: false,
 error: new ApiError(500, 'Network error'),
 };
 }
}

// 呼び出し側
const result = await fetchUser('123');

if (result.success) {
 console.log(result.data.name);
} else {
 console.error(result.error.message);
}

カスタムエラークラス

// ベースエラー
class AppError extends Error {
 constructor(
 message: string,
 public code: string,
 public statusCode: number = 500,
 public isOperational: boolean = true
 ) {
 super(message);
 this.name = this.constructor.name;
 Error.captureStackTrace(this, this.constructor);
 }
}

// 具体的なエラー
class ValidationError extends AppError {
 constructor(message: string, public fields: Record<string, string>) {
 super(message, 'VALIDATION_ERROR', 400);
 }
}

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

class UnauthorizedError extends AppError {
 constructor(message = 'Authentication required') {
 super(message, 'UNAUTHORIZED', 401);
 }
}

// 使用
throw new ValidationError('Invalid input', {
 email: 'Invalid email format',
 password: 'Too short',
});

グローバルエラーハンドラー

// Express.js
const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
 // ログ出力
 logger.error({
 err,
 requestId: req.headers['x-request-id'],
 path: req.path,
 method: req.method,
 });

 // 運用エラー(予期されるエラー)
 if (err instanceof AppError && err.isOperational) {
 return res.status(err.statusCode).json({
 error: {
 code: err.code,
 message: err.message,
 ...(err instanceof ValidationError && { fields: err.fields }),
 },
 });
 }

 // プログラムエラー(予期しないエラー)
 // 詳細は隠す
 return res.status(500).json({
 error: {
 code: 'INTERNAL_ERROR',
 message: 'An unexpected error occurred',
 },
 });
};

app.use(errorHandler);

Reactエラー境界

class ErrorBoundary extends React.Component<
 { children: React.ReactNode; fallback: React.ReactNode },
 { hasError: boolean; error: Error | null }
> {
 constructor(props) {
 super(props);
 this.state = { hasError: false, error: null };
 }

 static getDerivedStateFromError(error: Error) {
 return { hasError: true, error };
 }

 componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
 // エラー報告サービスに送信
 reportError(error, errorInfo);
 }

 render() {
 if (this.state.hasError) {
 return this.props.fallback;
 }
 return this.props.children;
 }
}

// 使用
<ErrorBoundary fallback={<ErrorPage />}>
 <App />
</ErrorBoundary>

注意: リトライは指数バックオフで間隔を広げないと、障害中のサービスにさらに負荷をかけてしまいます。またリトライ上限は必ず設定してください。

リトライパターン

interface RetryConfig {
 maxAttempts: number;
 baseDelay: number;
 maxDelay: number;
 retryableErrors?: string[];
}

async function withRetry<T>(
 fn: () => Promise<T>,
 config: RetryConfig
): Promise<T> {
 let lastError: Error;

 for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
 try {
 return await fn();
 } catch (error) {
 lastError = error as Error;

 // リトライ不可のエラー
 if (config.retryableErrors &&
 !config.retryableErrors.includes(error.code)) {
 throw error;
 }

 if (attempt === config.maxAttempts) break;

 // 指数バックオフ + ジッター
 const delay = Math.min(
 config.baseDelay * Math.pow(2, attempt - 1) + Math.random() * 1000,
 config.maxDelay
 );

 logger.warn({ attempt, delay, error: error.message }, 'Retrying...');
 await sleep(delay);
 }
 }

 throw lastError!;
}

// 使用
const data = await withRetry(
 () => fetchExternalApi(),
 { maxAttempts: 3, baseDelay: 1000, maxDelay: 10000 }
);

グレースフルデグラデーション

async function getProductWithFallback(id: string): Promise<Product> {
 try {
 // プライマリソース
 return await productService.get(id);
 } catch (error) {
 logger.warn({ id, error }, 'Primary source failed, trying cache');

 try {
 // キャッシュフォールバック
 const cached = await cache.get(`product:${id}`);
 if (cached) return cached;
 } catch (cacheError) {
 logger.warn({ id }, 'Cache also failed');
 }

 // 最終フォールバック:デフォルト値
 return getDefaultProduct(id);
 }
}

関連記事

まとめ

堅牢なエラーハンドリングは、Result型、カスタムエラークラス、グローバルハンドラー、リトライパターンを組み合わせて実現します。ユーザー体験と運用効率の両方を考慮した設計を心がけましょう

参考リソース

← 一覧に戻る
PR
PR
PR
PR