Skip to main content

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:
  • updateExternalCustomerError
  • updateExternalCustomerData?.updateExternalCustomer?.success
When the failure happens, it calls 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.tsx
  • apps/portal/src/pages/gate/acceptInvitation/error.tsx
These components keep the messaging and layout consistent.

Extending this system

Use this recipe when you add a new form or mutation and need consistent failure UX.
  1. Identify the failure source:
    • query: use*Query returns loading + error
    • mutation: use*Mutation returns loading + error + data?.success
  2. Decide what UI to show:
    • user-correctable form failure: show an Alert or helper text near the input
    • unexpected broken page: show InternalError
  3. Use shared safe messages:
Example: fall back to UNEXPECTED_ERROR_MESSAGE:
  1. 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.tsx for generic unexpected failures
  • apps/portal/src/pages/gate/acceptInvitation/error.tsx for invitation-specific messaging

What’s next?

Next, if you’re instrumenting UX, wire tracking so you can measure where users hit failures.