// Human error copy. Maps error codes / shapes / thrown values to a single // plain sentence a person can act on. Used by the root ErrorBoundary and // available to any route that catches a failure (e.g. a fetch reject). // // Ported/generalised from arcadia-personal-cloud-web's `acceptErrorMessage`: // switch on a known machine `code`, else flatten field-validation errors, // else fall back to the raw message or a friendly default. import { isRouteErrorResponse } from "react-router" /** A known machine error code → the human sentence we show for it. Extend * this map as the app grows real backend error codes. */ const CODE_MESSAGES: Record = { network_error: "Couldn't reach the server. Check your connection and try again.", timeout: "That took too long to respond. Try again in a moment.", unauthorized: "Your session has expired. Sign in again to continue.", forbidden: "You don't have access to that.", not_found: "We couldn't find what you were looking for.", rate_limited: "Too many requests just now. Give it a few seconds.", server_error: "Something went wrong on our end. Try again shortly.", } type ErrorShape = { error?: string code?: string message?: string errors?: Record } /** Turn any caught value into one human sentence. */ export function humanErrorMessage(err: unknown): string { // React Router route-error responses (thrown Responses / 404s etc.). if (isRouteErrorResponse(err)) { if (err.status === 404) return CODE_MESSAGES.not_found if (err.status === 401) return CODE_MESSAGES.unauthorized if (err.status === 403) return CODE_MESSAGES.forbidden if (err.status === 429) return CODE_MESSAGES.rate_limited if (err.status >= 500) return CODE_MESSAGES.server_error return err.statusText || "That request couldn't be completed." } const d = ( err && typeof err === "object" ? err : {} ) as ErrorShape const code = d.code ?? d.error if (code && CODE_MESSAGES[code]) return CODE_MESSAGES[code] // arcadia-style field validation: { errors: { field: [msg...] } } — // flatten into a single readable line so the user knows what to fix. if (d.errors && typeof d.errors === "object") { const lines: string[] = [] for (const [field, msgs] of Object.entries(d.errors)) { const label = field.replace(/_/g, " ") const arr = Array.isArray(msgs) ? msgs : [String(msgs)] for (const m of arr) lines.push(`${label} ${m}`) } if (lines.length) return lines.join("; ") } if (err instanceof Error && err.message) return err.message if (typeof d.message === "string" && d.message) return d.message return "An unexpected error occurred. Please try again." } /** Short heading to pair with {@link humanErrorMessage} in a boundary/card. */ export function humanErrorTitle(err: unknown): string { if (isRouteErrorResponse(err)) { if (err.status === 404) return "Page not found" return "Something went wrong" } return "Something went wrong" }