Frontend Error Handling
Error handling in the frontend should feel consistent: show loading states, surface safe user messages on failures, and route truly unexpected cases to shared error pages.How it works
Here’s the flow I look for when debugging “why did this form fail?” issues.1) Query errors: use error alongside loading
Query hooks follow the same returned shape (loading, error, data).
Pattern:
2) Mutation errors: check error or data?.<mutation>.success
Many mutations return { success, reason/result }-style payloads.
Example from checkout customer updates:
apps/portal/src/pages/admin/settings/billing/checkout/newPaymentForm/customerInfo/form.tsx
The form reacts in a useEffect by checking:
updateExternalCustomerErrorupdateExternalCustomerData?.updateExternalCustomer?.success
onErrorChange with a user-safe message, falling back to:
apps/portal/src/utils/checkout.ts (UNEXPECTED_ERROR_MESSAGE).
3) Shared error pages for “this should never happen”
If a page enters a broken state, use shared error UI like:apps/portal/src/pages/standard/internalError.tsxapps/portal/src/pages/gate/acceptInvitation/error.tsx
Extending this system
Use this recipe when you add a new form or mutation and need consistent failure UX.-
Identify the failure source:
- query:
use*Queryreturnsloading+error - mutation:
use*Mutationreturnsloading+error+data?.success
- query:
-
Decide what UI to show:
- user-correctable form failure: show an
Alertor helper text near the input - unexpected broken page: show
InternalError
- user-correctable form failure: show an
- Use shared safe messages:
UNEXPECTED_ERROR_MESSAGE:
- Verify:
What not to do
These rules keep error handling predictable and safe. ❌ Never show internal error stacks, SQL details, or raw GraphQL reason strings directly. ❌ Never gate entitlements/scopes in React error handlers; the server should enforce it and the UI should reflect the GraphQL contract. ❌ Never mix “loading” and “error” rendering in ways that leave the user without an actionable next step.File conventions
This section tells you where shared error UI and message constants should go. Prefer:apps/portal/src/pages/standard/internalError.tsxfor generic unexpected failuresapps/portal/src/pages/gate/acceptInvitation/error.tsxfor invitation-specific messaging
