Skip to main content
Back to Blog
Engineering

Error Handling That Respects Your Users

August 25, 202510 min read
Error HandlingUXTypeScriptReactBest Practices

Error Handling That Respects Your Users

`500 Internal Server Error`

That's what my trading dashboard showed for 2 hours while I was debugging a Supabase connection timeout. Two hours of users seeing a white page with a generic error message. No explanation, no guidance, no way to know if it was their fault or mine.

I fixed the bug in 20 minutes. Fixing the error handling took the rest of the day. And it was more important.

The Principles

1. Tell Users WHAT Happened (Not How)

```typescript // Terrible: exposes internals, helps nobody "Error: ECONNREFUSED 127.0.0.1:5432"

// Bad: accurate but unhelpful "Database connection failed"

// Good: user-centric, actionable "We're having trouble loading your data. This usually resolves in a few minutes. Your data is safe." ```

Users don't need to know your database is down. They need to know their data is safe and when to try again.

2. Tell Users WHAT TO DO Next

Every error message should have an action:

```typescript const errorResponses = { NETWORK_ERROR: { title: "Connection issue", message: "Check your internet and try again.", action: { label: "Retry", onClick: () => refetch() } }, AUTH_EXPIRED: { title: "Session expired", message: "Please log in again to continue.", action: { label: "Log In", onClick: () => redirect('/login') } }, RATE_LIMITED: { title: "Too many requests", message: "Please wait a moment and try again.", action: { label: "Retry in 30s", onClick: () => setTimeout(refetch, 30000) } }, SERVER_ERROR: { title: "Something went wrong", message: "We're looking into it. Try refreshing the page.", action: { label: "Refresh", onClick: () => location.reload() } } }; ```

3. Degrade Gracefully, Don't Crash Completely

When one part of the dashboard fails, don't blank the whole page:

```tsx function DashboardPage() { return (

<ErrorBoundary fallback={}>

  <ErrorBoundary fallback={<AlertsError />}>
    <ActiveAlerts />
  </ErrorBoundary>

  <ErrorBoundary fallback={<ChartError />}>
    <PriceChart />
  </ErrorBoundary>
</div>

); } ```

If the price chart API is down, the portfolio summary and alerts still work. Each section fails independently.

4. Log for Engineers, Display for Humans

```typescript try { const data = await fetchMarketData(symbol); return data; } catch (error) { // For engineers: full context in server logs console.error('Market data fetch failed', { symbol, error: error.message, stack: error.stack, timestamp: new Date().toISOString(), retryCount: attempt });

// For users: simple, helpful message throw new UserFacingError( "Market data temporarily unavailable", "Showing last known prices. Live data will resume automatically." ); } ```

The engineer gets the stack trace, the symbol, and the retry count. The user gets a human sentence and reassurance.

The Error Hierarchy

Not all errors are equal. I categorize them:

Category User Message Engineering Action
Transient (network, timeout) "Try again in a moment" Auto-retry with backoff
User error (bad input) "Please check [field]" Validate before submit
Auth (expired, revoked) "Please log in again" Redirect to login
System (our fault) "We're on it" Alert on-call, show cached data
Catastrophic (data loss risk) "Contact support: [email]" Page on-call immediately

Each category has a different tone, different recovery path, and different urgency.

The Test

My error handling test: deliberately break every external dependency (database, API, auth) and check:

  1. Does the user see a helpful message? (Not a stack trace)
  2. Does the user have a clear next action? (Not a dead end)
  3. Does the rest of the app still function? (Not a white screen)
  4. Did the engineers get alerted with full context? (Not a silent failure)

If all four pass, the error handling is solid. If any fail, I'm disrespecting either my users or my team.

Want to see this in action?

Check out the projects and case studies behind these articles.