feat: implement centralized error handling and logging with ErrorBoundary component

This commit is contained in:
2025-12-29 22:19:10 +00:00
parent b134f3f8d4
commit e2092d146d
5 changed files with 243 additions and 0 deletions

View File

@@ -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<Props, State> {
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 (
<Box sx={{ p: 3 }}>
<Alert severity="error" sx={{ mb: 2 }}>
<Typography variant="h6" gutterBottom>
Something went wrong
</Typography>
<Typography variant="body2" sx={{ mb: 2 }}>
{this.state.error?.message || 'An unexpected error occurred'}
</Typography>
<Button
variant="contained"
color="primary"
onClick={this.handleReset}
size="small"
>
Try Again
</Button>
</Alert>
</Box>
)
}
return this.props.children
}
}

View File

@@ -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<string, unknown>
}
/**
* 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'
}

View File

@@ -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<string, unknown>
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<APIErrorResponse> {
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 })
}

View File

@@ -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'

View File

@@ -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)
// }
}