Files
gridpilot.gg/apps/website/lib/api/base/ApiError.ts
2026-01-06 13:21:55 +01:00

155 lines
4.2 KiB
TypeScript

/**
* Enhanced API Error with detailed classification and context
*/
export type ApiErrorType =
| 'NETWORK_ERROR' // Connection failed, timeout, CORS
| 'AUTH_ERROR' // 401, 403 - Authentication/Authorization issues
| 'VALIDATION_ERROR' // 400 - Bad request, invalid data
| 'NOT_FOUND' // 404 - Resource not found
| 'SERVER_ERROR' // 500, 502, 503 - Server-side issues
| 'RATE_LIMIT_ERROR' // 429 - Too many requests
| 'CANCELED_ERROR' // Request was canceled
| 'TIMEOUT_ERROR' // Request timeout
| 'UNKNOWN_ERROR'; // Everything else
export interface ApiErrorContext {
endpoint?: string;
method?: string;
requestBody?: unknown;
timestamp: string;
statusCode?: number;
responseText?: string;
retryCount?: number;
wasRetry?: boolean;
troubleshooting?: string;
source?: string;
componentStack?: string;
isRetryable?: boolean;
isConnectivity?: boolean;
developerHint?: string;
}
export class ApiError extends Error {
public readonly type: ApiErrorType;
public readonly context: ApiErrorContext;
public readonly originalError?: Error;
constructor(
message: string,
type: ApiErrorType,
context: ApiErrorContext,
originalError?: Error
) {
super(message);
this.name = 'ApiError';
this.type = type;
this.context = context;
this.originalError = originalError;
// Maintains proper stack trace for where our error was thrown
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ApiError);
}
}
/**
* User-friendly message for production environments
*/
getUserMessage(): string {
switch (this.type) {
case 'NETWORK_ERROR':
return 'Unable to connect to the server. Please check your internet connection.';
case 'AUTH_ERROR':
return 'Authentication required. Please log in again.';
case 'VALIDATION_ERROR':
return 'The data you provided is invalid. Please check your input.';
case 'NOT_FOUND':
return 'The requested resource was not found.';
case 'SERVER_ERROR':
return 'Server is experiencing issues. Please try again later.';
case 'RATE_LIMIT_ERROR':
return 'Too many requests. Please wait a moment and try again.';
case 'TIMEOUT_ERROR':
return 'Request timed out. Please try again.';
case 'CANCELED_ERROR':
return 'Request was canceled.';
default:
return 'An unexpected error occurred. Please try again.';
}
}
/**
* Developer-friendly message with full context
*/
getDeveloperMessage(): string {
const base = `[${this.type}] ${this.message}`;
const ctx = [
this.context.method,
this.context.endpoint,
this.context.statusCode ? `status:${this.context.statusCode}` : null,
this.context.retryCount ? `retry:${this.context.retryCount}` : null,
]
.filter(Boolean)
.join(' ');
return ctx ? `${base} ${ctx}` : base;
}
/**
* Check if this error is retryable
*/
isRetryable(): boolean {
const retryableTypes: ApiErrorType[] = [
'NETWORK_ERROR',
'SERVER_ERROR',
'RATE_LIMIT_ERROR',
'TIMEOUT_ERROR',
];
return retryableTypes.includes(this.type);
}
/**
* Check if this error indicates connectivity issues
*/
isConnectivityIssue(): boolean {
return this.type === 'NETWORK_ERROR' || this.type === 'TIMEOUT_ERROR';
}
/**
* Get error severity for logging
*/
getSeverity(): 'error' | 'warn' | 'info' {
switch (this.type) {
case 'AUTH_ERROR':
case 'VALIDATION_ERROR':
case 'NOT_FOUND':
return 'warn';
case 'RATE_LIMIT_ERROR':
case 'CANCELED_ERROR':
return 'info';
default:
return 'error';
}
}
}
/**
* Type guards for error classification
*/
export function isApiError(error: unknown): error is ApiError {
return error instanceof ApiError;
}
export function isNetworkError(error: unknown): boolean {
return isApiError(error) && error.type === 'NETWORK_ERROR';
}
export function isAuthError(error: unknown): boolean {
return isApiError(error) && error.type === 'AUTH_ERROR';
}
export function isRetryableError(error: unknown): boolean {
return isApiError(error) && error.isRetryable();
}