Skip to content

Frontend Error Handling

Frontend Error Handling — Every Failable Action Surfaces an Error

Section titled “Frontend Error Handling — Every Failable Action Surfaces an Error”

No failable action may fail silently. Every network call, I/O, mutation, query, or async action that can reject/throw MUST have a user-visible error path. A swallowed error — empty catch, ignored isError, perpetual spinner, blank panel — is a bug, not a style nit.

  • Mutations (TanStack Query): handle onErrortoast.error(...) (sonner; the Toaster is mounted in __root.tsx). A mutation with no error path is incomplete.
  • Queries (TanStack Query): render the isError state explicitly — an inline error + retry affordance, NOT a perpetual spinner or empty content. Loading ≠ error.
  • Form submit / validation: field-level inline errors (react-hook-form + FormMessage) for user-fixable input; a form-level error for the submit call itself.
  • Unexpected render/runtime errors: an error boundary (route or app level) with a visible fallback — never a white screen.
  • Any imperative async (fetch, upload, action handler): wrap and surface the rejection — toast or inline.

When the failing response carries an X-Trace-ID header, the trace id is part of the error display’s design — render it per frontend-error-trace-id.md, which governs WHERE it appears and the public-page / form-field exceptions. This rule mandates that the error is shown at all; that rule mandates how the trace id rides along. Don’t re-decide the split here.

Use the existing surfaces — sonner toasts, react-hook-form FormMessage, shadcn inline alerts — per frontend-conventions.md. Don’t hand-roll bespoke error UI.