// utils/errorHandler.ts export interface ApiError { code: string; message: string; details?: string; field?: string; } export class ValidationError extends Error { public field?: string; constructor(msg: string, field?: string) { super(msg); this.name = "ValidationError"; this.field = field; } } export class NetworkError extends Error { constructor(msg: string) { super(msg); this.name = "NetworkError"; } } export function createValidationErrorResponse( msg: string, field?: string, ): Response { const error: ApiError = { code: "VALIDATION_ERROR", message: msg, ...(field && { field }), }; return new Response(JSON.stringify(error), { status: 400, headers: { "Content-Type": "application/json" }, }); } export function createNetworkErrorResponse( msg: string = "Network error", ): Response { return new Response(JSON.stringify({ code: "NETWORK_ERROR", message: msg }), { status: 503, headers: { "Content-Type": "application/json" }, }); } export function createTimeoutErrorResponse(ms: number): Response { return new Response( JSON.stringify({ code: "TIMEOUT_ERROR", message: "Request timed out after " + ms + "ms. Please try again.", }), { status: 504, headers: { "Content-Type": "application/json" } }, ); } export function validateRequiredString( value: unknown, fieldName: string, ): void { if (value === null || value === undefined || value === "") { throw new ValidationError(fieldName + " is required", fieldName); } if (typeof value !== "string") { throw new ValidationError(fieldName + " must be a string", fieldName); } } export function validateNumber( value: unknown, fieldName: string, opts?: { min?: number; max?: number; allowZero?: boolean }, ): number { if (value === null || value === undefined || value === "") { throw new ValidationError(fieldName + " is required", fieldName); } const numValue = typeof value === "string" ? parseFloat(value) : typeof value === "number" ? value : NaN; if (isNaN(numValue)) { throw new ValidationError(fieldName + " must be a valid number", fieldName); } if (opts?.min !== undefined && numValue < opts.min) { throw new ValidationError( fieldName + " must be at least " + opts.min, fieldName, ); } if (opts?.max !== undefined && numValue > opts.max) { throw new ValidationError( fieldName + " must be at most " + opts.max, fieldName, ); } if (!opts?.allowZero && numValue === 0) { throw new ValidationError( fieldName + " must be greater than zero", fieldName, ); } return numValue; } export function validateInteger( value: unknown, fieldName: string, opts?: { min?: number; max?: number; positive?: boolean }, ): number { const numValue = validateNumber(value, fieldName, opts); if (!Number.isInteger(numValue)) { throw new ValidationError(fieldName + " must be a whole number", fieldName); } if (opts?.positive && numValue <= 0) { throw new ValidationError( fieldName + " must be a positive integer", fieldName, ); } return numValue; } export function withTimeout( promise: Promise, timeoutMs: number, errMsg: string = "Timeout", ): Promise { return Promise.race([ promise, new Promise((_, reject) => setTimeout(() => reject(new Error(errMsg)), timeoutMs) ), ]); } export function handleApiError(error: unknown): Response { console.error("API Error:", error); if (error instanceof ValidationError) { return createValidationErrorResponse(error.message, error.field); } if (error instanceof NetworkError) { return createNetworkErrorResponse(error.message); } if (error instanceof Error) { if ( error.message.includes("timeout") || error.message.includes("timed out") ) { return createTimeoutErrorResponse(30000); } if ( error.message.includes("network") || error.message.includes("connection") ) { return createNetworkErrorResponse(error.message); } return new Response( JSON.stringify({ code: "INTERNAL_ERROR", message: "An unexpected error occurred", details: error.message, }), { status: 500, headers: { "Content-Type": "application/json" } }, ); } return new Response( JSON.stringify({ code: "INTERNAL_ERROR", message: "An unexpected error occurred", }), { status: 500, headers: { "Content-Type": "application/json" } }, ); }