From e2092d146d2bdf85ee3680bb7134ae04bdb2bb45 Mon Sep 17 00:00:00 2001 From: JohnDoe6345789 Date: Mon, 29 Dec 2025 22:19:10 +0000 Subject: [PATCH] feat: implement centralized error handling and logging with ErrorBoundary component --- .../atoms/feedback/ErrorBoundary.tsx | 83 +++++++++++++++++++ .../nextjs/src/lib/errors/format-error.ts | 46 ++++++++++ .../nextjs/src/lib/errors/handle-api-error.ts | 47 +++++++++++ frontends/nextjs/src/lib/errors/index.ts | 10 +++ frontends/nextjs/src/lib/errors/log-error.ts | 57 +++++++++++++ 5 files changed, 243 insertions(+) create mode 100644 frontends/nextjs/src/components/atoms/feedback/ErrorBoundary.tsx create mode 100644 frontends/nextjs/src/lib/errors/format-error.ts create mode 100644 frontends/nextjs/src/lib/errors/handle-api-error.ts create mode 100644 frontends/nextjs/src/lib/errors/index.ts create mode 100644 frontends/nextjs/src/lib/errors/log-error.ts diff --git a/frontends/nextjs/src/components/atoms/feedback/ErrorBoundary.tsx b/frontends/nextjs/src/components/atoms/feedback/ErrorBoundary.tsx new file mode 100644 index 000000000..c0c4aad25 --- /dev/null +++ b/frontends/nextjs/src/components/atoms/feedback/ErrorBoundary.tsx @@ -0,0 +1,83 @@ +'use client' + +import React, { Component, ReactNode } from 'react' +import { logError, LogLevel } from '@/lib/errors/log-error' +import { Alert, Button, Box, Typography } from '@mui/material' + +interface Props { + children: ReactNode + fallback?: ReactNode + onError?: (error: Error, errorInfo: React.ErrorInfo) => void +} + +interface State { + hasError: boolean + error?: Error +} + +/** + * Error Boundary component to catch React rendering errors + * Logs errors and displays a fallback UI + */ +export class ErrorBoundary extends Component { + constructor(props: Props) { + super(props) + this.state = { hasError: false } + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error } + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { + // Log error with component stack + logError( + error, + { + component: 'ErrorBoundary', + componentStack: errorInfo.componentStack, + }, + LogLevel.ERROR + ) + + // Call custom error handler if provided + this.props.onError?.(error, errorInfo) + } + + handleReset = (): void => { + this.setState({ hasError: false, error: undefined }) + } + + render(): ReactNode { + if (this.state.hasError) { + // Use custom fallback if provided + if (this.props.fallback) { + return this.props.fallback + } + + // Default error UI + return ( + + + + Something went wrong + + + {this.state.error?.message || 'An unexpected error occurred'} + + + + + ) + } + + return this.props.children + } +} diff --git a/frontends/nextjs/src/lib/errors/format-error.ts b/frontends/nextjs/src/lib/errors/format-error.ts new file mode 100644 index 000000000..6671d71c2 --- /dev/null +++ b/frontends/nextjs/src/lib/errors/format-error.ts @@ -0,0 +1,46 @@ +import { DBALError, DBALErrorCode } from '@/dbal/development/src/core/foundation/errors' +import { getErrorMessage, isError } from '@/lib/types/guards' + +export interface FormattedError { + message: string + code?: DBALErrorCode | number + stack?: string + details?: Record +} + +/** + * Format any error into a consistent structure + */ +export function formatError(error: unknown): FormattedError { + // Handle DBALError + if (error instanceof DBALError) { + return { + message: error.message, + code: error.code, + details: error.details, + } + } + + // Handle standard Error + if (isError(error)) { + return { + message: error.message, + stack: error.stack, + } + } + + // Handle error-like objects and other types + return { + message: getErrorMessage(error), + } +} + +/** + * Convert error to user-friendly message (safe for display) + */ +export function toUserMessage(error: unknown): string { + const formatted = formatError(error) + + // Don't expose stack traces to users + return formatted.message || 'An unexpected error occurred' +} diff --git a/frontends/nextjs/src/lib/errors/handle-api-error.ts b/frontends/nextjs/src/lib/errors/handle-api-error.ts new file mode 100644 index 000000000..57f1e25f1 --- /dev/null +++ b/frontends/nextjs/src/lib/errors/handle-api-error.ts @@ -0,0 +1,47 @@ +import { NextResponse } from 'next/server' +import { DBALError } from '@/dbal/development/src/core/foundation/errors' +import { formatError } from './format-error' + +export interface APIErrorResponse { + error: string + code?: number + details?: Record + timestamp?: string +} + +/** + * Centralized API error handler for consistent error responses + * Maps DBALError codes to HTTP status codes and returns standardized error responses + */ +export function handleAPIError(error: unknown): NextResponse { + const formatted = formatError(error) + + // Determine HTTP status code + let status = 500 + if (error instanceof DBALError) { + // Map DBALErrorCode to HTTP status + // Codes < 1000 are standard HTTP codes, use them directly + status = error.code < 1000 ? error.code : 500 + } + + // Create response + const response: APIErrorResponse = { + error: formatted.message, + code: formatted.code, + timestamp: new Date().toISOString(), + } + + // Add details in development only + if (process.env.NODE_ENV === 'development' && formatted.details) { + response.details = formatted.details + } + + // Log error server-side + console.error('[API Error]', { + message: formatted.message, + code: formatted.code, + stack: formatted.stack, + }) + + return NextResponse.json(response, { status }) +} diff --git a/frontends/nextjs/src/lib/errors/index.ts b/frontends/nextjs/src/lib/errors/index.ts new file mode 100644 index 000000000..baae8d32c --- /dev/null +++ b/frontends/nextjs/src/lib/errors/index.ts @@ -0,0 +1,10 @@ +export { formatError, toUserMessage } from './format-error' +export type { FormattedError } from './format-error' + +export { handleAPIError } from './handle-api-error' +export type { APIErrorResponse } from './handle-api-error' + +export { logError, LogLevel } from './log-error' +export type { LogContext } from './log-error' + +export { ErrorBoundary } from '@/components/atoms/feedback/ErrorBoundary' diff --git a/frontends/nextjs/src/lib/errors/log-error.ts b/frontends/nextjs/src/lib/errors/log-error.ts new file mode 100644 index 000000000..eb001cd77 --- /dev/null +++ b/frontends/nextjs/src/lib/errors/log-error.ts @@ -0,0 +1,57 @@ +import { formatError } from './format-error' + +export enum LogLevel { + ERROR = 'error', + WARN = 'warn', + INFO = 'info', + DEBUG = 'debug', +} + +export interface LogContext { + component?: string + userId?: string + action?: string + [key: string]: unknown +} + +/** + * Centralized error logging with consistent format + * Replaces scattered console.error calls throughout the codebase + */ +export function logError( + error: unknown, + context?: LogContext, + level: LogLevel = LogLevel.ERROR +): void { + const formatted = formatError(error) + + const logEntry = { + level, + timestamp: new Date().toISOString(), + message: formatted.message, + code: formatted.code, + context, + stack: formatted.stack, + } + + // Use appropriate console method + switch (level) { + case LogLevel.ERROR: + console.error('[Error]', logEntry) + break + case LogLevel.WARN: + console.warn('[Warning]', logEntry) + break + case LogLevel.INFO: + console.log('[Info]', logEntry) + break + case LogLevel.DEBUG: + console.log('[Debug]', logEntry) + break + } + + // TODO: Send to error tracking service (Sentry, etc.) + // if (process.env.NODE_ENV === 'production') { + // sendToErrorTracker(logEntry) + // } +}