🔥 本番環境で起きた悲劇:エラーハンドリング不足の代償

本番環境でのエラーハンドリング不足は深刻な問題を引き起こすことがあります。Slackに緊急アラートが鳴り響きます。「本番サイトが真っ白で何も表示されない」というユーザーからの報告が殺到していました。

💥 何が起きたのか

症状:アプリケーション全体が真っ白な画面になり、一切の操作が不可能に

原因:サードパーティAPIが一時的に500エラーを返したことで、ジェンスパーク(Genspark)が生成したデータフェッチコードが例外をスローし、それを補足する仕組みがなかったため、Reactのエラーバウンダリも設定されておらず、アプリ全体がクラッシュ

影響範囲:約2時間のサービス停止、数百人のユーザーに影響

学んだ教訓:「動くコード」と「本番で使えるコード」は全く別物である

この事件以降、開発者はジェンスパーク(Genspark)にコード生成を依頼する際、必ず「包括的なエラーハンドリングを含めて実装してください」と明示的に指示するようになりました。

🎯 エラーハンドリングの基本戦略:3層防御モデル

ジェンスパーク(Genspark)との協働で構築したエラーハンドリング戦略は、「3層防御モデル」と呼んでいるアプローチです。

1. 予防層(Preventive Layer):エラーを起こさない

最も効果的なエラーハンドリングは、そもそもエラーが発生しない設計です。

// ❌ 悪い例:入力検証なし function processUserData(data) { return data.name.toUpperCase(); // dataやdata.nameがundefinedだったらエラー } // ✅ 良い例:型ガードと早期リターン function processUserData(data: unknown): string | null { // 型ガードで安全性を確保 if (!data || typeof data !== 'object') { console.warn('Invalid data provided:', data); return null; } // 必要なプロパティの存在確認 if (!('name' in data) || typeof data.name !== 'string') { console.warn('Missing or invalid name property:', data); return null; } // 空文字チェック if (data.name.trim() === '') { console.warn('Empty name provided'); return null; } return data.name.toUpperCase(); } // さらに良い例:Zodなどのバリデーションライブラリを使用 import { z } from 'zod'; const UserSchema = z.object({ name: z.string().min(1, 'Name is required'), email: z.string().email('Invalid email format'), age: z.number().int().positive().optional(), }); function processUserData(data: unknown): string | null { try { const validData = UserSchema.parse(data); return validData.name.toUpperCase(); } catch (error) { if (error instanceof z.ZodError) { console.error('Validation failed:', error.errors); } return null; } }

🛡️ ジェンスパーク(Genspark)への指示例(予防層)

「以下の要件でデータ処理関数を実装してください:

  • TypeScriptの厳格モード(strict: true)に準拠
  • すべての入力パラメータに型ガードを実装
  • Zodバリデーションスキーマを使用
  • nullやundefinedのチェックを徹底
  • エッジケース(空配列、空文字、0、false)を適切に処理

2. 検出層(Detection Layer):エラーを早期に捕捉する

予防層をすり抜けたエラーは、できるだけ早い段階で検出し、適切にハンドリングします。

// API呼び出しのエラーハンドリングパターン interface ApiResponse<T> { data?: T; error?: ApiError; } interface ApiError { code: string; message: string; details?: Record<string, unknown>; } async function fetchUserData(userId: string): Promise<ApiResponse<User>> { try { const response = await fetch(`/api/users/${userId}`); // HTTPステータスコードのチェック if (!response.ok) { // エラー詳細を取得 const errorData = await response.json().catch(() => ({})); // ステータスコード別の処理 switch (response.status) { case 400: return { error: { code: 'BAD_REQUEST', message: 'Invalid user ID format', details: errorData, }, }; case 401: return { error: { code: 'UNAUTHORIZED', message: 'Authentication required', details: errorData, }, }; case 404: return { error: { code: 'NOT_FOUND', message: `User ${userId} not found`, details: errorData, }, }; case 429: return { error: { code: 'RATE_LIMIT', message: 'Too many requests. Please try again later.', details: errorData, }, }; case 500: case 502: case 503: return { error: { code: 'SERVER_ERROR', message: 'Server is temporarily unavailable', details: errorData, }, }; default: return { error: { code: 'UNKNOWN_ERROR', message: `Unexpected error: ${response.status}`, details: errorData, }, }; } } // レスポンスのパース const data = await response.json(); // データ検証 const validationResult = UserSchema.safeParse(data); if (!validationResult.success) { return { error: { code: 'INVALID_RESPONSE', message: 'Server returned invalid data format', details: { zodErrors: validationResult.error.errors }, }, }; } return { data: validationResult.data }; } catch (error) { // ネットワークエラー、タイムアウトなど if (error instanceof TypeError && error.message.includes('fetch')) { return { error: { code: 'NETWORK_ERROR', message: 'Network connection failed', details: { originalError: error.message }, }, }; } // その他の予期しないエラー return { error: { code: 'UNEXPECTED_ERROR', message: 'An unexpected error occurred', details: { originalError: error instanceof Error ? error.message : String(error), }, }, }; } } // 使用例 async function displayUserProfile(userId: string) { const result = await fetchUserData(userId); if (result.error) { // エラーコードに応じた処理 switch (result.error.code) { case 'NOT_FOUND': showNotFoundPage(); break; case 'UNAUTHORIZED': redirectToLogin(); break; case 'NETWORK_ERROR': showRetryDialog(); break; default: showGenericErrorMessage(result.error.message); } return; } // 正常処理 renderUserProfile(result.data); }

⚠️ よくある間違い:エラーの握りつぶし

以下のようなコードは絶対に避けてください:

// ❌ 最悪のパターン try { await someAsyncOperation(); } catch (error) { // 何もしない = エラーを握りつぶす } // ❌ やや良いが不十分 try { await someAsyncOperation(); } catch (error) { console.log(error); // ログだけで処理しない } // ✅ 適切な処理 try { await someAsyncOperation(); } catch (error) { // 1. ログ記録 logger.error('Operation failed', { error, context }); // 2. ユーザーへの通知 showErrorNotification('Operation failed. Please try again.'); // 3. リカバリー処理またはフォールバック await fallbackOperation(); }

3. 復旧層(Recovery Layer):エラーから回復する

エラーが検出された後、システムをどのように復旧させるかが最も重要です。

// 自動リトライ機能を持つフェッチ関数 interface RetryOptions { maxRetries: number; initialDelay: number; maxDelay: number; backoffMultiplier: number; retryableStatuses: number[]; } const DEFAULT_RETRY_OPTIONS: RetryOptions = { maxRetries: 3, initialDelay: 1000, // 1秒 maxDelay: 10000, // 10秒 backoffMultiplier: 2, retryableStatuses: [408, 429, 500, 502, 503, 504], }; async function fetchWithRetry<T>( url: string, options: RequestInit = {}, retryOptions: Partial<RetryOptions> = {} ): Promise<T> { const config = { ...DEFAULT_RETRY_OPTIONS, ...retryOptions }; let lastError: Error | null = null; for (let attempt = 0; attempt <= config.maxRetries; attempt++) { try { const response = await fetch(url, options); // リトライ対象のステータスコードかチェック if (!response.ok && config.retryableStatuses.includes(response.status)) { throw new Error(`HTTP ${response.status}`); } if (!response.ok) { // リトライ不可能なエラー(4xx系など) const errorData = await response.json().catch(() => ({})); throw new Error(`HTTP ${response.status}: ${JSON.stringify(errorData)}`); } return await response.json(); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); // 最後の試行なら例外をスロー if (attempt === config.maxRetries) { throw lastError; } // 指数バックオフでリトライ const delay = Math.min( config.initialDelay * Math.pow(config.backoffMultiplier, attempt), config.maxDelay ); console.warn( `Request failed (attempt ${attempt + 1}/${config.maxRetries + 1}). ` + `Retrying in ${delay}ms...`, { error: lastError.message } ); await new Promise(resolve => setTimeout(resolve, delay)); } } throw lastError || new Error('Unknown error'); } // フォールバック機能を持つデータフェッチ async function fetchUserDataWithFallback(userId: string): Promise<User> { try { // メインAPI return await fetchWithRetry<User>(`/api/v2/users/${userId}`); } catch (primaryError) { console.warn('Primary API failed, trying fallback...', primaryError); try { // フォールバックAPI(旧バージョン) return await fetchWithRetry<User>(`/api/v1/users/${userId}`); } catch (fallbackError) { console.warn('Fallback API failed, using cache...', fallbackError); try { // キャッシュから取得 const cachedData = await getCachedUser(userId); if (cachedData) { return cachedData; } } catch (cacheError) { console.error('Cache retrieval failed', cacheError); } // 全ての手段が失敗したら例外をスロー throw new Error( 'Failed to fetch user data from all sources. ' + 'Please check your connection and try again.' ); } } }

🎉 復旧層の実装効果

この3段階フォールバック機構を実装した結果:

  • エラー率:ユーザー体感エラーが大幅に削減
  • 可用性:APIの一時的障害時でもサービス継続
  • ユーザー満足度:「サービスが安定している」という評価が増加

🛡️ Reactコンポーネントのエラーハンドリング

4. Error Boundaryの実装

Reactアプリケーションでは、コンポーネントツリー内のエラーを補足するError Boundaryが不可欠です。

// ErrorBoundary.tsx import React, { Component, ErrorInfo, ReactNode } from 'react'; interface Props { children: ReactNode; fallback?: (error: Error, reset: () => void) => ReactNode; onError?: (error: Error, errorInfo: ErrorInfo) => void; } interface State { hasError: boolean; error: Error | null; } export class ErrorBoundary extends Component<Props, State> { constructor(props: Props) { super(props); this.state = { hasError: false, error: null, }; } static getDerivedStateFromError(error: Error): State { return { hasError: true, error, }; } componentDidCatch(error: Error, errorInfo: ErrorInfo) { // エラーログの送信 console.error('ErrorBoundary caught an error:', error, errorInfo); // カスタムエラーハンドラの呼び出し this.props.onError?.(error, errorInfo); // エラー監視サービスに送信(例:Sentry) if (typeof window !== 'undefined' && window.Sentry) { window.Sentry.captureException(error, { contexts: { react: { componentStack: errorInfo.componentStack, }, }, }); } } resetError = () => { this.setState({ hasError: false, error: null, }); }; render() { if (this.state.hasError && this.state.error) { // カスタムフォールバックUIがあれば使用 if (this.props.fallback) { return this.props.fallback(this.state.error, this.resetError); } // デフォルトのエラーUI return ( <div style={{ padding: '40px', textAlign: 'center', backgroundColor: '#fff5f5', border: '2px solid #fc8181', borderRadius: '8px', margin: '20px', }}> <h2 style={{ color: '#c53030', marginBottom: '16px' }}> エラーが発生しました </h2> <p style={{ color: '#742a2a', marginBottom: '24px' }}> {this.state.error.message} </p> <button onClick={this.resetError} style={{ padding: '10px 20px', backgroundColor: '#3182ce', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', }} > 再試行 </button> </div> ); } return this.props.children; } } // 使用例 function App() { return ( <ErrorBoundary fallback={(error, reset) => ( <CustomErrorPage error={error} onRetry={reset} /> )} onError={(error, errorInfo) => { logErrorToService({ error: error.message, stack: error.stack, componentStack: errorInfo.componentStack, }); }} > <AppContent /> </ErrorBoundary> ); }

5. 非同期エラーのハンドリング(React Query活用)

React Queryを使用することで、データフェッチのエラーハンドリングが劇的に簡単になります。

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; // カスタムフックでエラーハンドリングを統合 function useUserData(userId: string) { return useQuery({ queryKey: ['user', userId], queryFn: () => fetchUserData(userId), retry: 3, retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000), staleTime: 5 * 60 * 1000, // 5分間はキャッシュを使用 onError: (error) => { console.error('Failed to fetch user:', error); // エラー監視サービスに通知 reportError(error); }, }); } // コンポーネント内での使用 function UserProfile({ userId }: { userId: string }) { const { data, isLoading, isError, error, refetch } = useUserData(userId); // ローディング状態 if (isLoading) { return <LoadingSkeleton />; } // エラー状態 if (isError) { return ( <ErrorDisplay title="ユーザー情報の取得に失敗しました" message={error instanceof Error ? error.message : 'Unknown error'} onRetry={refetch} /> ); } // データがない場合 if (!data) { return <EmptyState message="ユーザーが見つかりませんでした" />; } // 正常表示 return <UserCard user={data} />; } // Mutationのエラーハンドリング function useUpdateUser() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (userData: UpdateUserData) => updateUser(userData), onSuccess: (data, variables) => { // キャッシュを更新 queryClient.setQueryData(['user', variables.userId], data); // 成功通知 showSuccessNotification('ユーザー情報を更新しました'); }, onError: (error, variables, context) => { // エラーログ console.error('Failed to update user:', error); // ユーザーに通知 showErrorNotification('更新に失敗しました。もう一度お試しください。'); // エラー監視サービスに報告 reportError(error); }, }); }

✨ React Query導入の効果

  • コード量削減:手動のエラーハンドリングコードが70%削減
  • 一貫性:アプリ全体で統一されたエラーハンドリング
  • UX向上:自動リトライとキャッシュで快適な操作感
  • 保守性:エラー処理ロジックの一元管理

📊 エラーのロギングと監視

6. 構造化ロギングの実装

本番環境では、エラーが発生したときの状況を正確に把握できるロギングが必須です。

// logger.ts enum LogLevel { DEBUG = 'DEBUG', INFO = 'INFO', WARN = 'WARN', ERROR = 'ERROR', } interface LogContext { userId?: string; sessionId?: string; requestId?: string; url?: string; userAgent?: string; [key: string]: unknown; } class Logger { private context: LogContext = {}; setContext(context: Partial<LogContext>) { this.context = { ...this.context, ...context }; } private log(level: LogLevel, message: string, data?: unknown) { const logEntry = { timestamp: new Date().toISOString(), level, message, context: this.context, data, }; // コンソールに出力 console.log(JSON.stringify(logEntry)); // 本番環境では外部サービスに送信 if (process.env.NODE_ENV === 'production') { this.sendToLoggingService(logEntry); } } debug(message: string, data?: unknown) { this.log(LogLevel.DEBUG, message, data); } info(message: string, data?: unknown) { this.log(LogLevel.INFO, message, data); } warn(message: string, data?: unknown) { this.log(LogLevel.WARN, message, data); } error(message: string, error: Error | unknown, data?: unknown) { const errorData = { ...data, error: { message: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, name: error instanceof Error ? error.name : undefined, }, }; this.log(LogLevel.ERROR, message, errorData); } private async sendToLoggingService(logEntry: unknown) { try { await fetch('/api/logs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(logEntry), }); } catch (error) { // ロギング自体の失敗は握りつぶす(無限ループ防止) console.error('Failed to send log:', error); } } } export const logger = new Logger(); // 使用例 async function processOrder(orderId: string) { logger.setContext({ orderId, userId: getCurrentUserId() }); try { logger.info('Starting order processing'); const order = await fetchOrder(orderId); logger.debug('Order fetched successfully', { order }); await validateOrder(order); logger.debug('Order validated'); await processPayment(order); logger.info('Payment processed successfully'); return order; } catch (error) { logger.error('Order processing failed', error, { stage: 'unknown', orderId, }); throw error; } }

🎯 ジェンスパーク(Genspark)に指示する際のベストプラクティス

✅ 包括的なエラーハンドリングを実装させる指示テンプレート

「以下の要件でXXX機能を実装してください:

  1. 予防層:すべての入力に型ガードとバリデーションを実装
  2. 検出層:try-catchで例外を補足し、エラー型に応じた分岐処理
  3. 復旧層:自動リトライ(最大3回、指数バックオフ)とフォールバック処理
  4. ユーザー通知:わかりやすいエラーメッセージとリトライボタンの表示
  5. ロギング:構造化ログでエラー詳細を記録
  6. 監視連携:重大なエラーは監視サービスに通知

各エラーケースで適切なHTTPステータスコードを返し、ユーザーへのフィードバックを必ず実装してください。」

💫 まとめ:エラーハンドリングはユーザー体験の一部

冒頭の本番障害から学んだ最大の教訓は、「エラーハンドリングは技術的な課題ではなく、ユーザー体験の一部である」ということです。

完璧なコードは存在しません。重要なのは、エラーが発生したときにシステムがどのように振る舞うかです。適切なエラーハンドリングを実装することで、予期しない問題が発生してもユーザーに安心感を与え、サービスの信頼性を保つことができます。

ジェンスパーク(Genspark)にコード生成を依頼する際は、機能の実装だけでなく、エラーハンドリング戦略も明確に指示することで、本番環境で安心して運用できるコードが生成されます。

次のステップ:既存のコードベースを見直し、エラーハンドリングが不足している部分を特定してみてください。そして、この記事で紹介した3層防御モデルを適用することで、より堅牢なアプリケーションに進化させることができるはずです。