/** * Standardized Error Handling * Provides consistent error types and handling patterns across the codebase */ export enum ErrorCode { // Authentication & Authorization UNAUTHENTICATED = 'UNAUTHENTICATED', FORBIDDEN = 'FORBIDDEN', UNAUTHORIZED = 'UNAUTHORIZED', // Validation BAD_USER_INPUT = 'BAD_USER_INPUT', VALIDATION_ERROR = 'VALIDATION_ERROR', // Not Found NOT_FOUND = 'NOT_FOUND', RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND', TENANT_NOT_FOUND = 'TENANT_NOT_FOUND', // Business Logic QUOTA_EXCEEDED = 'QUOTA_EXCEEDED', RESOURCE_CONFLICT = 'RESOURCE_CONFLICT', OPERATION_FAILED = 'OPERATION_FAILED', // External Services EXTERNAL_SERVICE_ERROR = 'EXTERNAL_SERVICE_ERROR', BLOCKCHAIN_ERROR = 'BLOCKCHAIN_ERROR', PROXMOX_ERROR = 'PROXMOX_ERROR', // System INTERNAL_ERROR = 'INTERNAL_ERROR', DATABASE_ERROR = 'DATABASE_ERROR', NETWORK_ERROR = 'NETWORK_ERROR', } export interface AppErrorOptions { code: ErrorCode message: string details?: unknown cause?: Error statusCode?: number } /** * Standardized application error */ export class AppError extends Error { public readonly code: ErrorCode public readonly details?: unknown public readonly cause?: Error public readonly statusCode: number constructor(options: AppErrorOptions) { super(options.message) this.name = 'AppError' this.code = options.code this.details = options.details this.cause = options.cause this.statusCode = options.statusCode || this.getDefaultStatusCode(options.code) // Maintain proper stack trace if (Error.captureStackTrace) { Error.captureStackTrace(this, AppError) } } private getDefaultStatusCode(code: ErrorCode): number { switch (code) { case ErrorCode.UNAUTHENTICATED: return 401 case ErrorCode.FORBIDDEN: case ErrorCode.UNAUTHORIZED: return 403 case ErrorCode.BAD_USER_INPUT: case ErrorCode.VALIDATION_ERROR: return 400 case ErrorCode.NOT_FOUND: case ErrorCode.RESOURCE_NOT_FOUND: case ErrorCode.TENANT_NOT_FOUND: return 404 case ErrorCode.RESOURCE_CONFLICT: return 409 case ErrorCode.QUOTA_EXCEEDED: return 429 case ErrorCode.EXTERNAL_SERVICE_ERROR: case ErrorCode.BLOCKCHAIN_ERROR: case ErrorCode.PROXMOX_ERROR: return 502 case ErrorCode.DATABASE_ERROR: case ErrorCode.NETWORK_ERROR: return 503 default: return 500 } } toJSON() { return { name: this.name, code: this.code, message: this.message, details: this.details, statusCode: this.statusCode, } } } /** * Helper functions for common error scenarios */ export const AppErrors = { unauthenticated: (message = 'Authentication required', details?: unknown) => new AppError({ code: ErrorCode.UNAUTHENTICATED, message, details }), forbidden: (message = 'Access denied', details?: unknown) => new AppError({ code: ErrorCode.FORBIDDEN, message, details }), badInput: (message: string, details?: unknown) => new AppError({ code: ErrorCode.BAD_USER_INPUT, message, details }), validation: (message: string, details?: unknown) => new AppError({ code: ErrorCode.VALIDATION_ERROR, message, details }), notFound: (resource: string, id?: string) => new AppError({ code: ErrorCode.RESOURCE_NOT_FOUND, message: id ? `${resource} with id ${id} not found` : `${resource} not found`, details: { resource, id }, }), tenantNotFound: (tenantId: string) => new AppError({ code: ErrorCode.TENANT_NOT_FOUND, message: `Tenant ${tenantId} not found`, details: { tenantId }, }), quotaExceeded: (message: string, details?: unknown) => new AppError({ code: ErrorCode.QUOTA_EXCEEDED, message, details }), conflict: (message: string, details?: unknown) => new AppError({ code: ErrorCode.RESOURCE_CONFLICT, message, details }), externalService: (service: string, message: string, cause?: Error) => new AppError({ code: ErrorCode.EXTERNAL_SERVICE_ERROR, message: `${service}: ${message}`, cause, details: { service }, }), blockchain: (message: string, cause?: Error) => new AppError({ code: ErrorCode.BLOCKCHAIN_ERROR, message, cause, }), proxmox: (message: string, cause?: Error) => new AppError({ code: ErrorCode.PROXMOX_ERROR, message, cause, }), database: (message: string, cause?: Error) => new AppError({ code: ErrorCode.DATABASE_ERROR, message, cause, }), internal: (message: string, cause?: Error, details?: unknown) => new AppError({ code: ErrorCode.INTERNAL_ERROR, message, cause, details, }), } /** * Check if error is an AppError */ export function isAppError(error: unknown): error is AppError { return error instanceof AppError } /** * Convert any error to AppError */ export function toAppError(error: unknown, defaultMessage = 'An error occurred'): AppError { if (isAppError(error)) { return error } if (error instanceof Error) { return AppErrors.internal(defaultMessage, error) } return AppErrors.internal(defaultMessage, undefined, { originalError: error }) }