React Error Boundaryの実務的な使い方|エラーハンドリング完全ガイド
1. Error Boundaryとは
React Error Boundaryは、子コンポーネント内で発生したJavaScriptエラーをキャッチし、エラーUIを表示するための仕組みです。従来のtry-catchではキャッチできないレンダリング中のエラーや、ライフサイクルメソッド内のエラーを処理できます。
本番環境では、エラーが発生するたびにアプリケーション全体がクラッシュするのを防ぐために、Error Boundaryの実装は必須です。
2. 業務でのユースケース
2.1 ダッシュボード画面での部分的なエラー
複数のウィジェットで構成されるダッシュボードで、1つのウィジェットのデータ取得に失敗しても、他のウィジェットは表示し続けたい場合があります。Error Boundaryでウィジェットを個別にラップすることで、部分的なエラーに対応できます。
2.2 サードパーティライブラリのエラー防止
Chart.jsやMap関連のライブラリを使用する際、不正なデータを渡した場合のエラーをError Boundaryでキャッチし、フォールバックUIを表示します。
2.3 エラーログの送信とモニタリング
Error Boundaryでエラーをキャッチした際に、Sentry やDatadogなどの監視ツールにエラーログを送信し、本番環境での問題を早期に検知します。
3. 実装コード
3.1 基本的なError Boundaryの実装(TypeScript)
import React, { ReactNode, ErrorInfo } from 'react';\n\ninterface Props {\n children: ReactNode;\n fallback?: ReactNode;\n onError?: (error: Error, errorInfo: ErrorInfo) => void;\n}\n\ninterface State {\n hasError: boolean;\n error: Error | null;\n}\n\nclass ErrorBoundary extends React.Component {\n constructor(props: Props) {\n super(props);\n this.state = {\n hasError: false,\n error: null,\n };\n }\n\n static getDerivedStateFromError(error: Error) {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo) {\n console.error('Error caught by boundary:', error, errorInfo);\n this.props.onError?.(error, errorInfo);\n }\n\n handleReset = () => {\n this.setState({ hasError: false, error: null });\n };\n\n render() {\n if (this.state.hasError) {\n return this.props.fallback || (\n \n エラーが発生しました
\n {this.state.error?.message}
\n \n \n );\n }\n\n return this.props.children;\n }\n}\n\nexport default ErrorBoundary;
3.2 本番環境向けのError Boundary実装
実務では、エラーを監視ツールに送信し、ユーザーには適切なメッセージを表示する必要があります。以下は、Sentryと連携した実装例です。
import React, { ReactNode, ErrorInfo } from 'react';\nimport * as Sentry from '@sentry/react';\n\ninterface Props {\n children: ReactNode;\n level?: 'widget' | 'page' | 'global';\n onError?: (error: Error, errorInfo: ErrorInfo) => void;\n}\n\ninterface State {\n hasError: boolean;\n errorId: string | null;\n}\n\nclass ProductionErrorBoundary extends React.Component {\n constructor(props: Props) {\n super(props);\n this.state = {\n hasError: false,\n errorId: null,\n };\n }\n\n static getDerivedStateFromError(error: Error) {\n return { hasError: true };\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo) {\n const errorId = Sentry.captureException(error, {\n contexts: {\n react: {\n componentStack: errorInfo.componentStack,\n },\n },\n tags: {\n errorLevel: this.props.level || 'global',\n },\n });\n\n this.setState({ errorId: errorId as string });\n this.props.onError?.(error, errorInfo);\n\n // ローカルストレージにエラー情報を保存\n const errorLog = {\n timestamp: new Date().toISOString(),\n message: error.message,\n stack: error.stack,\n sentryId: errorId,\n level: this.props.level,\n };\n\n const logs = JSON.parse(localStorage.getItem('errorLogs') || '[]');\n logs.push(errorLog);\n localStorage.setItem('errorLogs', JSON.stringify(logs.slice(-10)));\n }\n\n render() {\n if (this.state.hasError) {\n const level = this.props.level || 'global';\n\n if (level === 'widget') {\n return (\n \n \n このウィジェットの読み込みに失敗しました\n
\n {this.state.errorId && (\n \n Error ID: {this.state.errorId}\n \n )}\n \n );\n }\n\n if (level === 'page') {\n return (\n \n ページの読み込みに失敗しました
\n お手数ですが、ページを再度読み込んでください
\n \n \n );\n }\n\n return (\n \n 予期しないエラーが発生しました
\n 申し訳ございませんが、しばらく経ってからアクセスしてください
\n \n Error ID: {this.state.errorId}\n
\n \n \n );\n }\n\n return this.props.children;\n }\n}\n\nexport default ProductionErrorBoundary;
3.3 複数階層のError Boundary設定
実務では、複数階層のError Boundaryを組み合わせることで、きめ細かいエラーハンドリングを実現します。
// App.tsx\nimport React from 'react';\nimport ProductionErrorBoundary from './ErrorBoundary';\nimport Dashboard from './pages/Dashboard';\n\nfunction App() {\n return (\n \n \n My Application
\n \n \n \n \n \n );\n}\n\nexport default App;
// pages/Dashboard.tsx\nimport React from 'react';\nimport ProductionErrorBoundary from '../ErrorBoundary';\nimport SalesWidget from '../components/widgets/SalesWidget';\nimport UserActivityWidget from '../components/widgets/UserActivityWidget';\nimport RevenueChart from '../components/widgets/RevenueChart';\n\nfunction Dashboard() {\n return (\n \n \n \n \n\n \n \n \n\n \n \n \n \n );\n}\n\nexport default Dashboard;
3.4 非同期エラーハンドリングの実装
Error Boundaryは非同期処理(Promise内)のエラーをキャッチできないため、useErrorHandlerフックと組み合わせて実装します。
import React, { useEffect } from 'react';\nimport { useErrorHandler } from 'react-error-boundary';\n\ninterface FetchResult {\n id: number;\n name: string;\n value: number;\n}\n\nfunction UserDataComponent() {\n const [data, setData] = React.useState(null);\n const [loading, setLoading] = React.useState(true);\n const handleError = useErrorHandler();\n\n useEffect(() => {\n const fetchData = async () => {\n try {\n setLoading(true);\n const response = await fetch('/api/user-data');\n\n if (!response.ok) {\n throw new Error(\n `API error: ${response.status} ${response.statusText}`\n );\n }\n\n const result: FetchResult = await response.json();\n setData(result);\n } catch (error) {\n handleError(error);\n } finally {\n setLoading(false);\n }\n };\n\n fetchData();\n }, [handleError]);\n\n if (loading) return 読み込み中...;\n if (!data) return データなし;\n\n return (\n \n {data.name}
\n ID: {data.id}
\n Value: {data.value}
\n \n );\n}\n\nexport default UserDataComponent;
3.5 カスタムフックでのエラーハンドリング
複数コンポーネントで共通するAPI呼び出しの際は、カスタムフックでエラーハンドリングを一元化します。
// hooks/useApi.ts\nimport { useState, useCallback } from 'react';\nimport { useErrorHandler } from 'react-error-boundary';\n\ninterface UseApiOptions {\n retryCount?: number;\n retryDelay?: number;\n timeout?: number;\n}\n\nfunction useApi(\n url: string,\n options: UseApiOptions = {}\n) {\n const {\n retryCount = 3,\n retryDelay = 1000,\n timeout = 5000,\n } = options;\n\n const [data, setData] = useState(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState(null);\n const handleError = useErrorHandler();\n\n const fetchData = useCallback(async () => {\n let lastError: Error | null = null;\n\n for (let attempt = 0; attempt <= retryCount; attempt++) {\n try {\n setLoading(true);\n setError(null);\n\n const controller = new AbortController();\n const timeoutId = setTimeout(\n () => controller.abort(),\n timeout\n );\n\n const response = await fetch(url, {\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n throw new Error(\n `HTTP ${response.status}: ${response.statusText}`\n );\n }\n\n const result: T = await response.json();\n setData(result);\n return result;\n } catch (err) {\n lastError = err instanceof Error\n ? err\n : new Error(String(err));\n\n if (attempt < retryCount) {\n await new Promise((resolve) =>\n setTimeout(resolve, retryDelay * Math.pow(2, attempt))\n );\n }\n } finally {\n setLoading(false);\n }\n }\n\n if (lastError) {\n setError(lastError);\n handleError(lastError);\n }\n\n return null;\n }, [url, retryCount, retryDelay, timeout, handleError]);\n\n return { data, loading, error, fetchData };\n}\n\nexport default useApi;
4. よくある応用パターン
4.1 React Error Boundary + react-error-boundaryライブラリ
実務ではreact-error-boundaryライブラリを使用することで、より簡潔に実装できます。
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';\n\nfunction ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {\n return (\n\nエラーが発生しました
\n\n {error.message}\n\n \n
\n );\n}\n\nfunction App() {\n return (\n
{\n console.log('Error boundary caught:', error, errorInfo);\n // ここでエラーログをサーバーに送信\n }}\n onReset={() => {\n console.log('Error boundary reset');\n }}\n >\n \n );\n}\n\nexport default App;\n 4.2 エラー深刻度による処理振り分け
エラーの種類により、異なる処理を実行するパターンです。
import React, { ReactNode, ErrorInfo } from 'react';\n\ntype ErrorSeverity = 'critical' | 'warning' | 'info';\n\ninterface Props {\n children: ReactNode;\n getSeverity?: (error: Error) => ErrorSeverity;\n}\n\ninterface State {\n hasError: boolean;\n error: Error | null;\n severity: ErrorSeverity;\n}\n\nclass AdvancedErrorBoundary extends React.Component{\n constructor(props: Props) {\n super(props);\n this.state = {\n hasError: false,\n error: null,\n severity: 'info',\n };\n }\n\n static getDerivedStateFromError(error: Error) {\n let severity: ErrorSeverity = 'info';\n\n if (error.message.includes('timeout')) {\n severity = 'warning';\n } else if (error.message.includes('Network')) {\n severity = 'critical';\n }\n\n return { hasError: true, error, severity };\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo) {\n const severity = this.props.getSeverity?.(error) || this.state.severity;\n\n if (severity === 'critical') {\n // 重大なエラー: 管理者に即座に通知\n fetch('/api/notify-critical-error', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n error: error.message,\n stack: error.stack,\n timestamp: new Date().toISOString(),\n }),\n }).catch(console.error);\n } else if (severity === 'warning') {\n // 警告レベル: ログに記録するのみ\n console.warn('Warning level error:', error);\n }\n }\n\n render() {\n if (this.state.hasError) {\n const { severity } = this.state;\n const bgColors = {\n critical: '#f8d7da',\n warning: '#fff3cd',\n info: '#d1ecf1',\n };\n const textColors = {\n critical: '#721c24',\n warning: '#856404',\n info: '#0c5460',\n };\n\n return (\n \n\n );\n }\n\n return this.props.children;\n }\n}\n\nexport default AdvancedErrorBoundary;{severity.toUpperCase()}
\n{this.state.error?.message}
\n4.3 エラーリトライ機能の実装
特定のエラー(例:ネットワークエラー)は自動的にリトライするパターンです。
import React, { ReactNode, ErrorInfo } from 'react';\n\ninterface Props {\n children: ReactNode;\n maxRetries?: number;\n retryDelay?: number;\n}\n\ninterface State {\n hasError: boolean;\n error: Error | null;\n retryCount: number;\n}\n\nclass RetryableErrorBoundary extends React.Component{\n retryTimeoutId: NodeJS.Timeout | null = null;\n\n constructor(props: Props) {\n super(props);\n this.state = {\n hasError: false,\n error: null,\n retryCount: 0,\n };\n }\n\n static getDerivedStateFromError(error: Error) {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo) {\n const isRetryable =\n error.message.includes('Network') ||\n error.message.includes('timeout') ||\n error.message.includes('fetch');\n\n if (isRetryable && this.state.retryCount < (this.props.maxRetries || 3)) {\n this.retryTimeoutId = setTimeout(\n () => this.handleRetry(),\n this.props.retryDelay || 2000\n );\n }\n }\n\n handleRetry = () => {\n this.setState((prevState) => ({\n hasError: false,\n error: null,\n retryCount: prevState.retryCount + 1,\n }));\n };\n\n componentWillUnmount() {\n if (this.retryTimeoutId) {\n clearTimeout(this.retryTimeoutId);\n }\n }\n\n render() {\n if (this.state.hasError) {\n return (\n \n\n );\n }\n\n return this.props.children;\n }\n}\n\nexport default RetryableErrorBoundary;エラーが発生しました。リトライ中... ({this.state.retryCount})
\n \n5. 注意点と実装時のポイント
5.1 Error Boundaryでキャッチできないエラー
Error Boundaryは以下のエラーをキャッチできないため、別途対応が必要です。
- イベントハンドラ内のエラー:try-catchで対応
- 非同期処理のエラー:useErrorHandlerやPromiseの.catch()で対応
- サーバーサイドレンダリング時のエラー:サーバー側で個別にハンドリング
- Error Boundary自体のエラー:キャッチできないため、慎重に実装
// イベントハンドラのエラー処理\nfunction Button() {\n const handleClick = () => {\n try {\n // ボタンクリック処理\n throw new Error('Something went wrong');\n } catch (error) {\n console.error('Button error:', error);\n }\n };\n\n return ;\n}
5.2 パフォーマンスへの配慮
Error Boundaryはクラスコンポーネントのため、以下の点に注意します。
- Error Boundaryの作成は最小限にしぼる(過度な分割は避ける)
- getDerivedStateFromErrorではサイドエフェクトを実行しない
- componentDidCatchでの重い処理は非同期で実行する
5.3 テストの書き方
Error Boundaryのテストは、エラーをthrowするコンポーネントを作成して実施します。
// ErrorComponent.test.tsx\nimport { render, screen } from '@testing-library/react';\nimport ErrorBoundary from './ErrorBoundary';\n\nconst ThrowError = () => {\n throw new Error('Test error');\n};\n\ndescribe('ErrorBoundary', () => {\n beforeEach(() => {\n jest.spyOn(console, 'error').mockImplementation();\n });\n\n afterEach(() => {\n jest.restoreAllMocks();\n });\n\n test('should catch error and display fallback UI', () => {\n render(\n \n \n \n );\n\n expect(screen.getByText(/エラーが発生しました/i)).toBeInTheDocument();\n });\n\n test('should display custom fallback', () => {\n render(\n Custom Error UI

