React Error Boundaryの実務的な使い方|エラーハンドリング完全ガイド

React / Next.js

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 window.location.reload()}\n style={{\n padding: '10px 20px',\n backgroundColor: '#007bff',\n color: 'white',\n border: 'none',\n borderRadius: '4px',\n cursor: 'pointer',\n }}\n >\n ページを再度読み込む\n \n
\n );\n }\n\n return (\n
\n

予期しないエラーが発生しました

\n

申し訳ございませんが、しばらく経ってからアクセスしてください

\n

\n Error ID: {this.state.errorId}\n

\n (window.location.href = '/')}\n style={{\n padding: '10px 20px',\n backgroundColor: '#007bff',\n color: 'white',\n border: 'none',\n borderRadius: '4px',\n cursor: 'pointer',\n }}\n >\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}\n\nexport default App;

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

{severity.toUpperCase()}

\n

{this.state.error?.message}

\n
\n );\n }\n\n return this.props.children;\n }\n}\n\nexport default AdvancedErrorBoundary;

4.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

エラーが発生しました。リトライ中... ({this.state.retryCount})

\n \n
\n );\n }\n\n return this.props.children;\n }\n}\n\nexport default RetryableErrorBoundary;

5. 注意点と実装時のポイント

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
}>\n \n \n );\n\n expect(screen.getByText('Custom Error UI')).toBeInTheDocument();\n });\n});

5.4 ローカル開発時のエラー表示

本番環境とは異なり、開発環境ではエラースタックトレースを詳細に表示することが重要です。

class DevelopmentAwareErrorBoundary extends React.Component {\n  componentDidCatch(error: Error, errorInfo: ErrorInfo) {\n    if (process.env.NODE_ENV === 'development') {\n      console.error('Error details:', {\n        error,\n        componentStack: errorInfo.componentStack,\n        timestamp: new Date().toISOString(),\n      });\n    } else {\n      // 本番環境: エラーログサービスに送信\n      this.reportErrorToService(error, errorInfo);\n    }\n  }\n\n  private reportErrorToService(\n    error: Error,\n    errorInfo: ErrorInfo\n  ) {\n    // Sentry, Rollbar等に送信\n  }\n\n  render() {\n    if (this.state.hasError) {\n      if (process.env.NODE_ENV === 'development') {\n        return (\n          
\n

Development Error

\n {this.state.error?.stack}\n
\n );\n }\n\n return (\n
\n

エラーが発生しました

\n

申し訳ございません。問題を解決するため対応中です。

\n
\n );\n }\n\n return this.props.children;\n }\n}\n\nexport default DevelopmentAwareErrorBoundary;

6. 実務での推奨設定パターン

複雑なアプリケーションでは、複数階層のError Boundaryを組み合わせることが効果的です。


      
タイトルとURLをコピーしました