URL: https://wattfare.com/docs/guides/error-recovery
# Error recovery

> Every failure mode maps to a specific, recoverable piece of UI. This guide turns Wattfare's typed errors into states your users actually understand — connect, top up, slow down, or retry.

## The principle

Don't surface raw errors. Each Wattfare error code corresponds to exactly one user action. Your job is to translate code → action, on the server first (so the browser gets a stable code) and in the UI second (so the user gets a button).

| code | Meaning | The one action |
| --- | --- | --- |
| not_connected | No budget connected yet | Show “Connect AI budget” |
| funding_invalid | Their funding key broke | Show “Reconnect” (same button) |
| budget_exceeded | Monthly cap reached | Show usage; link to raise the cap |
| rate_limited | Too many requests | Back off / “slow down” message |
| network / upstream | Transient failure | Retry with backoff |
| auth | Your credentials are wrong | Log & alert — it's your bug, not theirs |

## Step 1 — normalise on the server

Recover the typed error and re-emit its JSON, so the browser always receives `{ error: { code, message } }` with a meaningful status instead of a generic 500.

```ts
// app/api/chat/route.ts — translate every failure into a stable client code.
import { toWattfareError } from "wattfare/server";

try {
  const result = streamText({ model: ai.model(MODEL), messages });
  return result.toTextStreamResponse();
} catch (err) {
  const wf = toWattfareError(err);
  if (!wf) throw err; // not ours — let the platform 500 it

  // Re-emit the typed error so the browser gets a code it can switch on.
  return Response.json(wf.toJSON(), { status: wf.status || 502 });
}
```

## Step 2 — switch in the UI

Read the code and move to a named state. No string-matching on messages.

```ts
async function send(prompt: string) {
  const res = await fetch("/api/chat", {
    method: "POST",
    body: JSON.stringify({ prompt }),
  });

  if (!res.ok) {
    const { error } = await res.json();
    switch (error?.code) {
      case "not_connected":
      case "funding_invalid":
        return setState({ kind: "needs_connect" });
      case "budget_exceeded":
        return setState({ kind: "out_of_budget" });
      case "rate_limited":
        return setState({ kind: "slow_down" });
      default:
        return setState({ kind: "error", message: error?.message });
    }
  }

  // …stream the successful response
}
```

## Connect vs. reconnect

`not_connected` and `funding_invalid` both mean “the user must act before this works,” which is why `isNotConnected()` matches both. The same connect button fixes both — only the copy changes.

```tsx
// funding_invalid means the *user's* funding broke (revoked key, no credit).
// It's caught by isNotConnected, so the same "Connect" button fixes it —
// just lead with a clearer message.
const { connect } = useWattfare();

if (state.kind === "needs_connect") {
  return (
    <div>
      <p>Reconnect your AI budget to continue.</p>
      <button onClick={connect}>⚡ Connect AI budget</button>
    </div>
  );
}
```

## Out of budget

On `budget_exceeded`, show the user where they stand and where to fix it. Pull the numbers from `status.usage.monthlyUsd` and `status.limits.monthlyUsd`, and link them to their [budget page](https://wattfare.com/account) to raise the cap.

> **Warn before you block**
>
> Don't wait for the hard 402. When `remainingUsd` drops below a threshold (say $1), surface a gentle “running low” hint so the wall never comes as a surprise. See [Budget UI patterns](https://wattfare.com/docs/guides/budget-ui).

## Retrying transient failures

Only `rate_limited`, `network`, and `upstream` are worth retrying — the rest need a human. Back off exponentially with a little jitter:

```ts
// Exponential backoff for transient failures (network / upstream / rate limit).
async function withRetry<T>(fn: () => Promise<T>, tries = 3): Promise<T> {
  let lastErr: unknown;
  for (let i = 0; i < tries; i++) {
    try {
      return await fn();
    } catch (err) {
      lastErr = err;
      const wf = toWattfareError(err);
      const retryable = wf?.code === "rate_limited" || wf?.code === "network" || wf?.code === "upstream";
      if (!retryable) throw err;
      await new Promise((r) => setTimeout(r, 2 ** i * 400 + Math.random() * 200));
    }
  }
  throw lastErr;
}
```

> **Never retry a 402**
>
> `not_connected`, `funding_invalid`, and `budget_exceeded` won't resolve by retrying — they need the user. Retrying just burns rate-limit budget and delays the fix.
