diff --git a/src/ui/errors/ErrorBoundary.tsx b/src/ui/errors/ErrorBoundary.tsx new file mode 100644 index 00000000..b9e8989f --- /dev/null +++ b/src/ui/errors/ErrorBoundary.tsx @@ -0,0 +1,56 @@ +import { Component } from "react"; +import type { ReactNode, ErrorInfo } from "react"; + +export interface ErrorBoundaryProps { + children: ReactNode; + fallback?: ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; +} + +/** + * React error boundary that catches render-time exceptions in the subtree. + * Displays a minimal fallback UI with a "Retry" button that resets state. + * + * Must be a class component — React requires componentDidCatch / getDerivedStateFromError. + */ +export class ErrorBoundary extends Component { + override state: ErrorBoundaryState = { hasError: false, error: null }; + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + override componentDidCatch(error: Error, info: ErrorInfo): void { + // Phase 1G-logger will replace this with useLogger() context logging. + // For now, write to console so errors are not silently swallowed. + console.error("[ErrorBoundary]", error, info.componentStack); + } + + private readonly handleRetry = (): void => { + this.setState({ hasError: false, error: null }); + }; + + override render(): ReactNode { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback; + } + + return ( +
+

Something went wrong

+

{this.state.error?.message}

+ +
+ ); + } + + return this.props.children; + } +}