turash/bugulma/frontend/lib/error-handling.ts
Damir Mukimov 18cdcb12fd
fix: continue linting fixes - remove unused variables, fix i18n strings
- Remove unused imports and variables from DashboardPage, HeritageBuildingPage, MatchesMapView
- Fix i18n literal strings in NetworkGraph component
- Continue systematic reduction of linting errors
2025-12-25 00:32:40 +01:00

438 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Error Handling Utilities
* Provides consistent error handling, logging, and user-friendly messaging across the application
*/
import { ApiError } from '@/lib/http-client';
import { z } from 'zod';
// Error severity levels
export enum ErrorSeverity {
LOW = 'low', // User can retry, minor inconvenience
MEDIUM = 'medium', // User experience impacted but recoverable
HIGH = 'high', // Critical functionality broken
CRITICAL = 'critical', // Application unusable
}
// Error categories for better handling
export enum ErrorCategory {
NETWORK = 'network',
AUTHENTICATION = 'authentication',
AUTHORIZATION = 'authorization',
VALIDATION = 'validation',
SERVER = 'server',
CLIENT = 'client',
TIMEOUT = 'timeout',
RATE_LIMIT = 'rate_limit',
UNKNOWN = 'unknown',
}
// Enhanced error interface
export interface AppError {
id: string;
message: string;
userMessage: string;
technicalMessage?: string;
category: ErrorCategory;
severity: ErrorSeverity;
statusCode?: number;
timestamp: Date;
context?: Record<string, unknown>;
retryable: boolean;
originalError?: Error;
}
// Error handler configuration
interface ErrorHandlerConfig {
enableLogging: boolean;
enableSentry?: boolean;
enableAnalytics?: boolean;
userMessageOverrides?: Record<string, string>;
}
/**
* Error Handler Class
*/
export class ErrorHandler {
private config: Required<Omit<ErrorHandlerConfig, 'enableSentry' | 'enableAnalytics'>> &
Pick<ErrorHandlerConfig, 'enableSentry' | 'enableAnalytics'>;
constructor(config: ErrorHandlerConfig = {}) {
this.config = {
enableLogging: import.meta.env.DEV,
enableSentry: false,
enableAnalytics: false,
userMessageOverrides: {},
...config,
};
}
/**
* Handle any error and convert to AppError
*/
handle(error: unknown, context?: Record<string, unknown>): AppError {
const appError = this.normalizeError(error, context);
// Log error
if (this.config.enableLogging) {
this.logError(appError);
}
// Send to error tracking services
if (this.config.enableSentry) {
this.sendToSentry(appError);
}
if (this.config.enableAnalytics) {
this.sendToAnalytics(appError);
}
return appError;
}
/**
* Convert various error types to AppError
*/
private normalizeError(error: unknown, context?: Record<string, unknown>): AppError {
const timestamp = new Date();
const id = this.generateErrorId();
// Handle ApiError
if (error instanceof ApiError) {
return {
id,
message: error.message,
userMessage: this.getUserMessage(error),
technicalMessage: import.meta.env.DEV ? error.data?.error : undefined,
category: this.categorizeApiError(error),
severity: this.getSeverity(error),
statusCode: error.status,
timestamp,
context,
retryable: error.retryable,
originalError: error,
};
}
// Handle ZodError with Zod v4's prettifyError
if (error && typeof error === 'object' && 'name' in error && error.name === 'ZodError') {
const zodError = error as z.ZodError;
// Use Zod v4's prettifyError if available (check if it exists on z object)
let prettifiedError: string;
try {
// @ts-expect-error - prettifyError may not be in type definitions yet
prettifiedError =
typeof z.prettifyError === 'function' ? z.prettifyError(zodError) : zodError.message;
} catch {
prettifiedError = zodError.message;
}
return {
id,
message: 'Validation failed',
userMessage: 'Please check your input and try again.',
technicalMessage: import.meta.env.DEV ? prettifiedError : undefined,
category: ErrorCategory.VALIDATION,
severity: ErrorSeverity.MEDIUM,
timestamp,
context,
retryable: false,
originalError: zodError,
};
}
// Handle standard Error
if (error instanceof Error) {
return {
id,
message: error.message,
userMessage: this.getGenericUserMessage(error),
technicalMessage: import.meta.env.DEV ? error.stack : undefined,
category: this.categorizeGenericError(error),
severity: ErrorSeverity.MEDIUM,
timestamp,
context,
retryable: this.isRetryableError(error),
originalError: error,
};
}
// Handle string error
if (typeof error === 'string') {
return {
id,
message: error,
userMessage: error,
category: ErrorCategory.UNKNOWN,
severity: ErrorSeverity.MEDIUM,
timestamp,
context,
retryable: false,
};
}
// Handle unknown error
return {
id,
message: 'An unknown error occurred',
userMessage: 'Something went wrong. Please try again.',
category: ErrorCategory.UNKNOWN,
severity: ErrorSeverity.MEDIUM,
timestamp,
context,
retryable: true,
};
}
/**
* Categorize API errors
*/
private categorizeApiError(error: ApiError): ErrorCategory {
switch (error.status) {
case 0:
return ErrorCategory.NETWORK;
case 401:
return ErrorCategory.AUTHENTICATION;
case 403:
return ErrorCategory.AUTHORIZATION;
case 408:
case 504:
return ErrorCategory.TIMEOUT;
case 429:
return ErrorCategory.RATE_LIMIT;
case 422:
return ErrorCategory.VALIDATION;
case 400:
case 404:
return ErrorCategory.CLIENT;
default:
return error.status >= 500 ? ErrorCategory.SERVER : ErrorCategory.CLIENT;
}
}
/**
* Categorize generic errors
*/
private categorizeGenericError(error: Error): ErrorCategory {
const message = error.message.toLowerCase();
if (message.includes('network') || message.includes('fetch')) {
return ErrorCategory.NETWORK;
}
if (message.includes('timeout')) {
return ErrorCategory.TIMEOUT;
}
if (message.includes('validation') || message.includes('invalid')) {
return ErrorCategory.VALIDATION;
}
return ErrorCategory.UNKNOWN;
}
/**
* Get severity level for API errors
*/
private getSeverity(error: ApiError): ErrorSeverity {
if (error.status >= 500) return ErrorSeverity.HIGH;
if (error.status === 429) return ErrorSeverity.MEDIUM;
if (error.status === 401 || error.status === 403) return ErrorSeverity.HIGH;
if (error.status === 0) return ErrorSeverity.CRITICAL; // Network completely down
return ErrorSeverity.LOW;
}
/**
* Check if generic error is retryable
*/
private isRetryableError(error: Error): boolean {
const message = error.message.toLowerCase();
return (
message.includes('network') || message.includes('timeout') || message.includes('temporary')
);
}
/**
* Get user-friendly message for API errors
*/
private getUserMessage(error: ApiError): string {
// Check for custom overrides
if (this.config.userMessageOverrides[error.status]) {
return this.config.userMessageOverrides[error.status];
}
// Use the ApiError's user-friendly message if available
if (error.message && !error.message.includes('HTTP')) {
return error.message;
}
// Default messages based on status
switch (error.status) {
case 0:
return 'Unable to connect. Please check your internet connection.';
case 401:
return 'Please log in to continue.';
case 403:
return "You don't have permission to perform this action.";
case 404:
return 'The requested item was not found.';
case 408:
case 504:
return 'The request timed out. Please try again.';
case 429:
return 'Too many requests. Please wait a moment before trying again.';
case 422:
return 'Please check your input and try again.';
case 500:
return 'A server error occurred. Please try again later.';
case 502:
case 503:
return 'Service temporarily unavailable. Please try again later.';
default:
return 'Something went wrong. Please try again.';
}
}
/**
* Get user-friendly message for generic errors
*/
private getGenericUserMessage(error: Error): string {
const message = error.message.toLowerCase();
if (message.includes('network') || message.includes('fetch')) {
return 'Network connection failed. Please check your internet connection.';
}
if (message.includes('timeout')) {
return 'The request timed out. Please try again.';
}
if (message.includes('validation')) {
return 'Please check your input and try again.';
}
return 'An unexpected error occurred. Please try again.';
}
/**
* Log error with appropriate level
*/
private logError(error: AppError): void {
const logData = {
id: error.id,
message: error.message,
category: error.category,
severity: error.severity,
statusCode: error.statusCode,
retryable: error.retryable,
context: error.context,
timestamp: error.timestamp.toISOString(),
};
switch (error.severity) {
case ErrorSeverity.CRITICAL:
console.error('🚨 CRITICAL ERROR:', logData);
break;
case ErrorSeverity.HIGH:
console.error('❌ HIGH PRIORITY ERROR:', logData);
break;
case ErrorSeverity.MEDIUM:
console.warn('⚠️ MEDIUM PRIORITY ERROR:', logData);
break;
default:
console.info(' LOW PRIORITY ERROR:', logData);
}
// Log technical details in development
if (import.meta.env.DEV && error.technicalMessage) {
console.debug('Technical details:', error.technicalMessage);
}
}
/**
* Send error to Sentry (placeholder)
*/
private sendToSentry(error: AppError): void {
// Placeholder for Sentry integration
if (typeof window !== 'undefined') {
const sentry = (
window as Window & {
Sentry?: { captureException: (error: Error, context?: Record<string, unknown>) => void };
}
).Sentry;
if (sentry) {
sentry.captureException(error.originalError || new Error(error.message), {
tags: {
category: error.category,
severity: error.severity,
},
extra: {
errorId: error.id,
context: error.context,
},
});
}
}
}
/**
* Send error to analytics (placeholder)
*/
private sendToAnalytics(error: AppError): void {
// Placeholder for analytics integration
if (typeof window !== 'undefined') {
const gtag = (
window as Window & {
gtag?: (event: string, name: string, params: Record<string, unknown>) => void;
}
).gtag;
if (gtag) {
gtag('event', 'exception', {
description: error.message,
fatal: error.severity === ErrorSeverity.CRITICAL,
});
}
}
}
/**
* Generate unique error ID
*/
private generateErrorId(): string {
return `error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}
// Global error handler instance
export const errorHandler = new ErrorHandler({
enableLogging: import.meta.env.DEV,
});
/**
* Hook for handling errors in React components
*/
export function useErrorHandler() {
return {
handleError: (error: unknown, context?: Record<string, unknown>) =>
errorHandler.handle(error, context),
};
}
/**
* Utility function for consistent error handling in async operations
*/
export async function withErrorHandling<T>(
operation: () => Promise<T>,
context?: Record<string, unknown>
): Promise<T> {
try {
return await operation();
} catch (error) {
const appError = errorHandler.handle(error, context);
throw appError;
}
}
/**
* Error boundary helper for consistent error reporting
*/
export function reportError(error: unknown, context?: Record<string, unknown>): AppError {
return errorHandler.handle(error, context);
}