From 62c79a500d3a7e8c0b091eb244a52bee31d0200a Mon Sep 17 00:00:00 2001 From: karilint Date: Fri, 29 May 2026 11:15:27 +0300 Subject: [PATCH] feat: Add ErrorBoundary and RouterError for better error UX Implement #1185: Provide ErrorBoundary and errorElement to catch and display application errors gracefully. - Add ErrorBoundary component to catch React component render errors - Add RouterError component to catch React Router v6 data router errors - Wrap RouterProvider with ErrorBoundary in main.tsx - Add errorElement to root route in router This provides a better user experience when the application encounters errors, showing a user-friendly error page with options to reload or go back instead of the default React error screen. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- .../ErrorBoundary/ErrorBoundary.tsx | 108 ++++++++++++++++++ .../components/ErrorBoundary/RouterError.tsx | 80 +++++++++++++ frontend/src/main.tsx | 9 +- frontend/src/router/index.tsx | 2 + 4 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/ErrorBoundary/ErrorBoundary.tsx create mode 100644 frontend/src/components/ErrorBoundary/RouterError.tsx diff --git a/frontend/src/components/ErrorBoundary/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary/ErrorBoundary.tsx new file mode 100644 index 00000000..215a1403 --- /dev/null +++ b/frontend/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -0,0 +1,108 @@ +import { Component, ErrorInfo, ReactNode } from 'react' + +interface Props { + children: ReactNode +} + +interface State { + hasError: boolean + error: Error | null +} + +export class ErrorBoundary extends Component { + public state: State = { + hasError: false, + error: null, + } + + public static getDerivedStateFromError(error: Error): State { + return { hasError: true, error } + } + + public componentDidCatch(_error: Error, _errorInfo: ErrorInfo): void { + // Error is logged to console by React in development + // In production, consider logging to an error tracking service + } + + public render(): ReactNode { + if (this.state.hasError) { + return ( +
+

Something went wrong

+

The application encountered an unexpected error.

+ + {this.state.error && ( +
+ Error Details +
{this.state.error.toString()}
+

{this.state.error.stack}

+
+ )} +
+ ) + } + + return this.props.children + } +} + +const styles = { + container: { + display: 'flex', + flexDirection: 'column' as const, + alignItems: 'center', + justifyContent: 'center', + height: '100vh', + padding: '2rem', + textAlign: 'center' as const, + backgroundColor: '#f5f5f5', + }, + heading: { + fontSize: '2rem', + color: '#d32f2f', + marginBottom: '1rem', + }, + message: { + fontSize: '1.2rem', + color: '#555', + marginBottom: '2rem', + }, + button: { + padding: '0.75rem 1.5rem', + fontSize: '1rem', + backgroundColor: '#1976d2', + color: 'white', + border: 'none', + borderRadius: '4px', + cursor: 'pointer', + }, + details: { + marginTop: '2rem', + textAlign: 'left' as const, + maxWidth: '800px', + }, + summary: { + cursor: 'pointer', + color: '#1976d2', + fontWeight: 'bold' as const, + }, + pre: { + whiteSpace: 'pre-wrap' as const, + wordWrap: 'break-word' as const, + backgroundColor: '#fff', + padding: '1rem', + borderRadius: '4px', + border: '1px solid #ddd', + }, + stack: { + whiteSpace: 'pre-wrap' as const, + wordWrap: 'break-word' as const, + fontSize: '0.875rem', + color: '#757575', + marginTop: '1rem', + }, +} + +export default ErrorBoundary diff --git a/frontend/src/components/ErrorBoundary/RouterError.tsx b/frontend/src/components/ErrorBoundary/RouterError.tsx new file mode 100644 index 00000000..de96239b --- /dev/null +++ b/frontend/src/components/ErrorBoundary/RouterError.tsx @@ -0,0 +1,80 @@ +import { useRouteError } from 'react-router-dom' + +export const RouterError = () => { + const error = useRouteError() + + // Handle different error types that useRouteError can return + const errorMessage = + error instanceof Error + ? error.message + : error instanceof Response + ? `${error.status} ${error.statusText}` + : typeof error === 'string' + ? error + : 'An unknown error occurred' + + return ( +
+

Navigation Error

+

The page could not be loaded.

+

{errorMessage}

+ + +
+ ) +} + +const styles = { + container: { + display: 'flex', + flexDirection: 'column' as const, + alignItems: 'center', + justifyContent: 'center', + height: '100vh', + padding: '2rem', + textAlign: 'center' as const, + backgroundColor: '#f5f5f5', + }, + heading: { + fontSize: '2rem', + color: '#d32f2f', + marginBottom: '1rem', + }, + message: { + fontSize: '1.2rem', + color: '#555', + marginBottom: '1rem', + }, + errorMessage: { + fontSize: '1rem', + color: '#757575', + marginBottom: '2rem', + fontFamily: 'monospace', + }, + button: { + padding: '0.75rem 1.5rem', + fontSize: '1rem', + backgroundColor: '#1976d2', + color: 'white', + border: 'none', + borderRadius: '4px', + cursor: 'pointer', + margin: '0.5rem', + }, + secondaryButton: { + padding: '0.75rem 1.5rem', + fontSize: '1rem', + backgroundColor: 'transparent', + color: '#1976d2', + border: '1px solid #1976d2', + borderRadius: '4px', + cursor: 'pointer', + margin: '0.5rem', + }, +} + +export default RouterError diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index b0753c57..ef153658 100755 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -5,11 +5,14 @@ import { Provider } from 'react-redux' import './styles/global.css' import { RouterProvider } from 'react-router-dom' import router from './router' +import { ErrorBoundary } from './components/ErrorBoundary/ErrorBoundary' ReactDOM.createRoot(document.getElementById('root')!).render( - - - + + + + + ) diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx index 06b3f4ce..14b1f74d 100644 --- a/frontend/src/router/index.tsx +++ b/frontend/src/router/index.tsx @@ -1,5 +1,6 @@ import { Navigate, createBrowserRouter } from 'react-router-dom' import App from '../App' +import { RouterError } from '../components/ErrorBoundary/RouterError' const loadPagesElement = async ( key: @@ -25,6 +26,7 @@ const router = createBrowserRouter([ { path: '/', element: , + errorElement: , children: [ { index: true,