All files / src/components ErrorBoundary.tsx

100% Statements 15/15
100% Branches 7/7
100% Functions 6/6
100% Lines 15/15

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118                                                                20x 20x       18x       9x     9x     1x 1x       47x 47x   47x   27x 3x       24x                                                                                 1x                     20x      
import { Component, type ReactNode, type ErrorInfo } from 'react'
import { AlertTriangle, RefreshCw } from 'lucide-react'
 
interface Props {
  children: ReactNode
  fallback?: ReactNode
}
 
interface State {
  hasError: boolean
  error: Error | null
  errorInfo: ErrorInfo | null
}
 
/**
 * Error boundary component that catches JavaScript errors in child components.
 *
 * Displays a user-friendly error UI instead of crashing the entire app.
 * Users can click "Try Again" to reset the error state and remount children.
 *
 * @example
 * <ErrorBoundary>
 *   <App />
 * </ErrorBoundary>
 *
 * // Or with custom fallback
 * <ErrorBoundary fallback={<CustomErrorUI />}>
 *   <App />
 * </ErrorBoundary>
 */
export default class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = { hasError: false, error: null, errorInfo: null }
  }
 
  static getDerivedStateFromError(error: Error): Partial<State> {
    return { hasError: true, error }
  }
 
  componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
    this.setState({ errorInfo })
 
    // Log error for debugging
    console.error('ErrorBoundary caught an error:', error, errorInfo)
  }
 
  handleReset = (): void => {
    this.setState({ hasError: false, error: null, errorInfo: null })
  }
 
  render(): ReactNode {
    const { hasError, error } = this.state
    const { children, fallback } = this.props
 
    if (hasError) {
      // Use custom fallback if provided
      if (fallback) {
        return fallback
      }
 
      // Default error UI
      return (
        <div className="flex min-h-screen flex-col items-center justify-center bg-background p-4">
          <div className="w-full max-w-md rounded-xl border border-border bg-card p-8 text-center shadow-lg">
            {/* Icon */}
            <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-destructive/10">
              <AlertTriangle className="h-8 w-8 text-destructive" />
            </div>
 
            {/* Title */}
            <h1 className="mb-2 text-xl font-semibold text-foreground">
              Something went wrong
            </h1>
 
            {/* Description */}
            <p className="mb-6 text-sm text-muted-foreground">
              An unexpected error occurred. This has been logged for debugging.
            </p>
 
            {/* Error details (development only) */}
            {import.meta.env.DEV && error && (
              <div className="mb-6 rounded-lg bg-muted p-3 text-left">
                <p className="mb-1 text-xs font-medium text-muted-foreground">
                  Error Details
                </p>
                <code className="text-xs text-destructive">
                  {error.name}: {error.message}
                </code>
              </div>
            )}
 
            {/* Actions */}
            <div className="flex flex-col gap-2">
              <button
                onClick={this.handleReset}
                className="flex w-full items-center justify-center gap-2 rounded-lg bg-primary px-4 py-2.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
              >
                <RefreshCw className="h-4 w-4" />
                Try Again
              </button>
 
              <button
                onClick={() => window.location.reload()}
                className="w-full rounded-lg bg-muted px-4 py-2.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted/80 hover:text-foreground"
              >
                Reload Page
              </button>
            </div>
          </div>
        </div>
      )
    }
 
    return children
  }
}
 
← Back to Dashboard