turash/bugulma/frontend/lib/error-handling.ts
Damir Mukimov 6347f42e20
Consolidate repositories: Remove nested frontend .git and merge into main repository
- Remove nested git repository from bugulma/frontend/.git
- Add all frontend files to main repository tracking
- Convert from separate frontend/backend repos to unified monorepo
- Preserve all frontend code and development history as tracked files
- Eliminate nested repository complexity for simpler development workflow

This creates a proper monorepo structure with frontend and backend
coexisting in the same repository for easier development and deployment.
2025-11-25 06:02:57 +01:00

410 lines
11 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';
// 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
if (error && typeof error === 'object' && 'name' in error && error.name === 'ZodError') {
const zodError = error as { message: string; issues?: unknown[] };
return {
id,
message: 'Validation failed',
userMessage: 'Please check your input and try again.',
technicalMessage: import.meta.env.DEV ? zodError.message : undefined,
category: ErrorCategory.VALIDATION,
severity: ErrorSeverity.MEDIUM,
timestamp,
context,
retryable: false,
originalError: new Error(zodError.message),
};
}
// 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' && (window as any).Sentry) {
(window as any).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' && (window as any).gtag) {
(window as any).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);
}