import { TRPCClientError } from "@trpc/client"; /** * Standard utility to parse errors from tRPC, HTTP responses, or native Error objects * and extract user-friendly messages, status codes, and field validation errors. */ export interface FormattedError { title: string; message: string; statusCode?: number; code?: string; requestId?: string; fieldErrors?: Record; isNetworkError: boolean; isRateLimit: boolean; isAuthError: boolean; isForbidden: boolean; isNotFound: boolean; } export function parseError(error: unknown, fallbackMessage = "An unexpected error occurred"): FormattedError { let title = "Error"; let message = fallbackMessage; let statusCode: number | undefined; let code: string | undefined; let requestId: string | undefined; let fieldErrors: Record | undefined; let isNetworkError = false; if (!error) { return { title, message, isNetworkError: false, isRateLimit: false, isAuthError: false, isForbidden: false, isNotFound: false, }; } // Handle tRPC Client Error if (error instanceof TRPCClientError || (typeof error === "object" && error !== null && "data" in error)) { const trpcErr = error as any; code = trpcErr.data?.code; statusCode = trpcErr.data?.httpStatus; if (trpcErr.message) { message = trpcErr.message; } // Check for zod validation errors if (trpcErr.data?.zodError) { const zodData = trpcErr.data.zodError; if (typeof zodData === "object" && zodData.fieldErrors) { fieldErrors = {}; for (const [field, msgs] of Object.entries(zodData.fieldErrors)) { if (Array.isArray(msgs) && msgs.length > 0) { fieldErrors[field] = msgs[0] as string; } } message = "Please check the highlighted fields and try again."; title = "Validation Failed"; } else if (typeof zodData === "string") { try { const parsed = JSON.parse(zodData); if (Array.isArray(parsed) && parsed.length > 0) { message = parsed[0].message || message; } } catch { message = zodData; } } } } else if (error instanceof Error) { message = error.message; // Check for network fetch failures if ( message.includes("Failed to fetch") || message.includes("NetworkError") || message.includes("Load failed") ) { isNetworkError = true; title = "Connection Lost"; message = "Unable to connect to the server. Please verify your internet connection."; } } else if (typeof error === "string") { message = error; } else if (typeof error === "object" && error !== null) { const errObj = error as Record; if (errObj.message) message = String(errObj.message); if (errObj.code) code = String(errObj.code); if (errObj.statusCode) statusCode = Number(errObj.statusCode); if (errObj.requestId) requestId = String(errObj.requestId); } // Status code / Code specific titling & messages const isRateLimit = statusCode === 429 || code === "TOO_MANY_REQUESTS" || message.toLowerCase().includes("rate limit"); const isAuthError = statusCode === 401 || code === "UNAUTHORIZED"; const isForbidden = statusCode === 403 || code === "FORBIDDEN"; const isNotFound = statusCode === 404 || code === "NOT_FOUND"; if (isRateLimit) { title = "Rate Limit Reached"; message = "You have made too many requests. Please pause for a few seconds before trying again."; } else if (isAuthError) { title = "Authentication Required"; message = "Your session has expired or you are not logged in. Please sign in to continue."; } else if (isForbidden) { title = "Access Denied"; message = "You do not have permission to perform this action. You may need to upgrade your subscription plan or ask a workspace admin."; } else if (isNotFound) { title = "Resource Not Found"; } return { title, message, statusCode, code, requestId, fieldErrors, isNetworkError, isRateLimit, isAuthError, isForbidden, isNotFound, }; } export function formatErrorMessage(error: unknown, fallbackMessage?: string): string { return parseError(error, fallbackMessage).message; }