🔥 本番環境で起きた悲劇:エラーハンドリング不足の代償
本番環境でのエラーハンドリング不足は深刻な問題を引き起こすことがあります。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機能を実装してください:
- 予防層:すべての入力に型ガードとバリデーションを実装
- 検出層:try-catchで例外を補足し、エラー型に応じた分岐処理
- 復旧層:自動リトライ(最大3回、指数バックオフ)とフォールバック処理
- ユーザー通知:わかりやすいエラーメッセージとリトライボタンの表示
- ロギング:構造化ログでエラー詳細を記録
- 監視連携:重大なエラーは監視サービスに通知
各エラーケースで適切なHTTPステータスコードを返し、ユーザーへのフィードバックを必ず実装してください。」
💫 まとめ:エラーハンドリングはユーザー体験の一部
冒頭の本番障害から学んだ最大の教訓は、「エラーハンドリングは技術的な課題ではなく、ユーザー体験の一部である」ということです。
完璧なコードは存在しません。重要なのは、エラーが発生したときにシステムがどのように振る舞うかです。適切なエラーハンドリングを実装することで、予期しない問題が発生してもユーザーに安心感を与え、サービスの信頼性を保つことができます。
ジェンスパーク(Genspark)にコード生成を依頼する際は、機能の実装だけでなく、エラーハンドリング戦略も明確に指示することで、本番環境で安心して運用できるコードが生成されます。
次のステップ:既存のコードベースを見直し、エラーハンドリングが不足している部分を特定してみてください。そして、この記事で紹介した3層防御モデルを適用することで、より堅牢なアプリケーションに進化させることができるはずです。