---
name: react-error-handling
description: >-
  Error boundaries, offline-first states, structured error reporting, and
  retry policy for React and React Native. Use when adding error handling,
  reviewing failure paths, or designing offline behaviour.
argument-hint: "[feature or screen]"
license: MIT
metadata:
  author: John Felix Lim
  version: "1.0.0"
---
# Error Handling

Apply when writing failure paths or reviewing a feature for robustness. On mobile, treat offline as a normal operating condition rather than an exceptional one.

## Rules

1. **Contain failures at a meaningful boundary** — One boundary at the app root turns any component error into a blank screen. Boundaries per screen and around each risky widget mean a failing recommendations carousel costs you a carousel, not a session.
2. **Mobile is offline-first whether you designed for it or not** — Lifts, tunnels, and rural coverage are normal operating conditions. A spinner that never resolves is the most common way this failure shows up, and it is indistinguishable from a hung app.
3. **Show users what to do, log engineers what happened** — A raw stack trace helps nobody in the UI, and 'Something went wrong' helps nobody in your logs. Split the audience: an actionable message on screen, full context in the report.
4. **Never swallow an error silently** — An empty `catch` converts a loud failure into a mysterious one. If an error is genuinely safe to ignore, the comment explaining why is mandatory.
5. **Every retry needs a backoff and a ceiling** — Immediate infinite retries turn a brief server wobble into a self-inflicted denial of service, and drain the battery while doing it.

## Patterns

### Boundary granularity

**Avoid** — Maximum blast radius. A non-essential widget can take down the entire session.

```tsx
<ErrorBoundary fallback={<BlankScreen />}>
  <App />
</ErrorBoundary>

// One bad render anywhere = the whole app is a blank screen.
```

**Prefer** — Nested boundaries scale the fallback to what actually broke.

```tsx
<ErrorBoundary fallback={<AppCrashScreen />}>
  <Navigation>
    <ErrorBoundary fallback={<ScreenError onRetry={reset} />}>
      <ProductScreen>
        {/* Non-critical widget fails alone. */}
        <ErrorBoundary fallback={null}>
          <Recommendations />
        </ErrorBoundary>
      </ProductScreen>
    </ErrorBoundary>
  </Navigation>
</ErrorBoundary>
```

### Offline as a designed state

**Avoid** — Offline is rendered as an infinite loading state, which users read as 'broken'.

```tsx
const { data, isPending } = useProducts();

if (isPending) return <Spinner />;
// No connection → pending forever. Looks like a frozen app.
return <ProductList data={data} />;
```

**Prefer** — Offline is a first-class state with a way forward, and cached content still renders.

```tsx
const isConnected = useNetInfo().isConnected;
const { data, isPending, error, refetch } = useProducts();

if (!isConnected && !data) {
  return <OfflineState onRetry={refetch} />;
}
if (isPending) return <ProductListSkeleton />;
if (error) return <ErrorState error={error} onRetry={refetch} />;

// Cached data with a clear staleness signal beats an empty screen.
return (
  <>
    {!isConnected && <OfflineBanner />}
    <ProductList data={data} />
  </>
);
```

### Logging with context

**Avoid** — The log has no identifying information, so the report is unactionable and the toast is unhelpful.

```ts
try {
  await submitOrder(draft);
} catch (e) {
  console.log("error");        // useless
  showToast("Something went wrong");
}
```

**Prefer** — One error, two audiences, each getting what they can act on.

```ts
try {
  await submitOrder(draft);
} catch (error) {
  // Engineers get everything needed to reproduce it.
  reportError(error, {
    tags: { feature: "checkout" },
    extra: { orderId: draft.id, itemCount: draft.items.length },
  });

  // Users get a specific, actionable message.
  showToast(
    isNetworkError(error)
      ? "You appear to be offline. Your order was saved as a draft."
      : "We couldn't place your order. Please try again.",
  );
}
```

## Review checklist

- [ ] Error boundaries wrap each screen and each non-critical widget.
- [ ] Offline is a designed state, never an endless spinner.
- [ ] No empty catch blocks; deliberate ignores carry a comment.
- [ ] Error reports include feature tags and identifying context.
- [ ] User-facing messages say what happened and what to do next.
- [ ] Retries use exponential backoff with a maximum attempt count.

---

From the John Felix Lim Frontend Engineering Playbook — https://github.com/JohnFelixLim