Files
gridpilot.gg/apps/website/lib/utils/errorUtils.ts
2026-01-01 20:31:05 +01:00

274 lines
7.1 KiB
TypeScript

/**
* Enhanced Error Utilities for GridPilot
*
* Provides comprehensive error handling, validation, and user-friendly error messages
* for both end users and developers.
*/
import { ApiError } from '@/lib/api/base/ApiError';
export interface ValidationError {
field: string;
message: string;
value?: unknown;
}
export interface EnhancedErrorContext {
timestamp?: string;
component?: string;
action?: string;
formData?: Record<string, unknown>;
userId?: string;
sessionId?: string;
}
/**
* Parse API error response to extract validation errors or user-friendly messages
*/
export function parseApiError(error: unknown): {
userMessage: string;
developerMessage: string;
validationErrors: ValidationError[];
isValidationError: boolean;
} {
const result = {
userMessage: 'An unexpected error occurred',
developerMessage: '',
validationErrors: [] as ValidationError[],
isValidationError: false,
};
if (error instanceof ApiError) {
result.developerMessage = error.getDeveloperMessage();
// Check if it's a validation error
if (error.type === 'VALIDATION_ERROR') {
result.isValidationError = true;
result.userMessage = 'Please check your input and try again';
// Try to parse validation details from response
try {
if (error.context.responseText) {
const parsed = JSON.parse(error.context.responseText);
// Handle NestJS validation error format
if (parsed.message && Array.isArray(parsed.message)) {
result.validationErrors = parsed.message.map((msg: any) => ({
field: msg.property || msg.field || 'unknown',
message: msg.constraints ? Object.values(msg.constraints).join(', ') : msg.message || 'Invalid value',
value: msg.value,
}));
}
// Handle custom error format
else if (parsed.errors && Array.isArray(parsed.errors)) {
result.validationErrors = parsed.errors.map((err: any) => ({
field: err.field || err.property || 'unknown',
message: err.message || 'Invalid value',
value: err.value,
}));
}
// Handle single message
else if (parsed.message && typeof parsed.message === 'string') {
result.userMessage = parsed.message;
}
}
} catch (e) {
// If parsing fails, use default messages
}
} else {
result.userMessage = error.getUserMessage();
}
} else if (error instanceof Error) {
result.userMessage = error.message;
result.developerMessage = error.message;
} else {
result.userMessage = 'An unknown error occurred';
result.developerMessage = String(error);
}
return result;
}
/**
* Format validation errors for display in forms
*/
export function formatValidationErrorsForForm(
validationErrors: ValidationError[]
): Record<string, string> {
const formErrors: Record<string, string> = {};
validationErrors.forEach((error) => {
// Map API field names to form field names
const fieldName = mapApiFieldToFormField(error.field);
formErrors[fieldName] = error.message;
});
return formErrors;
}
/**
* Map API field names to form field names
*/
function mapApiFieldToFormField(apiField: string): string {
const fieldMap: Record<string, string> = {
'rememberMe': 'rememberMe',
'email': 'email',
'password': 'password',
'displayName': 'displayName',
'firstName': 'firstName',
'lastName': 'lastName',
'confirmPassword': 'confirmPassword',
'role': 'role',
};
return fieldMap[apiField] || apiField;
}
/**
* Create enhanced error context for debugging
*/
export function createErrorContext(
error: unknown,
context: EnhancedErrorContext
): EnhancedErrorContext {
return {
timestamp: new Date().toISOString(),
...context,
};
}
/**
* Check if error is retryable
*/
export function isRetryable(error: unknown): boolean {
if (error instanceof ApiError) {
return error.isRetryable();
}
return false;
}
/**
* Check if error is a network connectivity issue
*/
export function isConnectivityError(error: unknown): boolean {
if (error instanceof ApiError) {
return error.isConnectivityIssue();
}
return false;
}
/**
* Get error severity for logging and display
*/
export function getErrorSeverity(error: unknown): 'error' | 'warning' | 'info' {
if (error instanceof ApiError) {
const severity = error.getSeverity();
if (severity === 'error') return 'error';
if (severity === 'warn') return 'warning';
return 'info';
}
return 'error';
}
/**
* Create user-friendly error summary
*/
export function createUserErrorSummary(error: unknown): {
title: string;
description: string;
action: string;
} {
const parsed = parseApiError(error);
if (parsed.isValidationError) {
return {
title: 'Invalid Input',
description: parsed.userMessage,
action: 'Please review your input and try again',
};
}
if (isConnectivityError(error)) {
return {
title: 'Connection Issue',
description: 'Unable to connect to the server',
action: 'Check your internet connection and try again',
};
}
if (isRetryable(error)) {
return {
title: 'Temporary Issue',
description: parsed.userMessage,
action: 'Please try again in a moment',
};
}
return {
title: 'Error',
description: parsed.userMessage,
action: 'Please try again or contact support if the issue persists',
};
}
/**
* Log error with context (only in development)
*/
export function logErrorWithContext(
error: unknown,
context: EnhancedErrorContext
): void {
if (process.env.NODE_ENV !== 'development') return;
const parsed = parseApiError(error);
const severity = getErrorSeverity(error);
console.group(`🚨 [${severity.toUpperCase()}] ${context.component || 'Unknown'} - ${context.action || 'Unknown action'}`);
console.error('User Message:', parsed.userMessage);
console.error('Developer Message:', parsed.developerMessage);
if (parsed.validationErrors.length > 0) {
console.error('Validation Errors:', parsed.validationErrors);
}
if (context.formData) {
console.error('Form Data:', context.formData);
}
console.error('Context:', context);
console.error('Original Error:', error);
console.groupEnd();
}
/**
* Delay execution for retry logic
*/
export async function delay(ms: number): Promise<void> {
await new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Retry operation with exponential backoff
*/
export async function retryWithBackoff<T>(
operation: () => Promise<T>,
maxRetries: number = 3,
baseDelay: number = 1000
): Promise<T> {
let lastError: unknown;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (attempt === maxRetries || !isRetryable(error)) {
throw error;
}
// Exponential backoff: 1s, 2s, 4s
const delayMs = baseDelay * Math.pow(2, attempt);
await delay(delayMs);
}
}
throw lastError;
}