Error handling
Every SDK error extends WattfareError. The “not connected” case is the expected first-run path, so it's modeled as a branch — not a crash. Helpers recover typed errors even when the AI SDK wraps them.
Error classes
Typed subclasses are exported from every entry point (server, client, react).
| Class | code | HTTP | When it happens |
|---|---|---|---|
| WattfareNotConnectedError | not_connected | 402 | The user hasn't connected a budget to your app yet. |
| WattfareBudgetExceededError | budget_exceeded | 402 | Their monthly cap for this period is reached. |
| WattfareFundingError | funding_invalid | 402 | Their funding key was rejected upstream (revoked / out of credit). They must reconnect. |
| WattfareRateLimitError | rate_limited | 429 | Too many requests. Back off and retry. |
| WattfareGrantError | grant_invalid | 402 | The one-time access grant is unknown, expired, fully used, or over its dollar cap. |
| WattfareAuthError | auth | 401 | Invalid or missing credentials (secret key, session token…). |
| WattfareNetworkError | network | 0 | Couldn't reach the Wattfare service at all. |
A generic WattfareError also covers invalid_request,
upstream (the model provider failed), and unknown.
// Every WattfareError serialises to this shape over the wire:
{ "error": { "code": "budget_exceeded", "message": "Spending cap reached for this period." } }
// And carries:
err.code // WattfareErrorCode
err.status // HTTP status it maps to
err.toJSON() Helper functions
Reach for these instead of instanceof. They see through the AI SDK's
APICallError wrapper, which would otherwise hide the real code.
| Function | Returns | Use it to… |
|---|---|---|
| toWattfareError(err) | WattfareError | null | Recover a typed error from anything. null when unrelated to Wattfare. |
| isBudgetExceeded(err) | boolean | Detect a reached cap (even AI-SDK-wrapped). |
| isNotConnected(err) | boolean | Detect “user must (re)connect” — matches both not_connected and funding_invalid. |
The canonical pattern
Handle the two actionable cases first, then fall back to the typed error's own status:
import { isNotConnected, isBudgetExceeded, toWattfareError } from "wattfare/server";
try {
const result = await generateText({
model: ai.model("anthropic/claude-sonnet-4"),
prompt,
});
return Response.json({ text: result.text });
} catch (err) {
if (isNotConnected(err)) return Response.json({ error: "connect_required" }, { status: 402 });
if (isBudgetExceeded(err)) return Response.json({ error: "budget_reached" }, { status: 402 });
const wf = toWattfareError(err);
if (wf) return Response.json(wf.toJSON(), { status: wf.status });
throw err; // genuinely unrelated — let it bubble
} Branching on code
When you need finer control, read code off the recovered error:
import { toWattfareError } from "wattfare/server";
try {
await generateText({ model: ai.model("openai/gpt-4o-mini"), prompt });
} catch (err) {
// err is the AI SDK's APICallError — a plain instanceof check would miss it.
const wf = toWattfareError(err);
if (wf?.code === "budget_exceeded") return askUserToRaiseCap();
if (wf?.code === "funding_invalid") return askUserToReconnect();
throw err;
} What to show users
| Condition | User-facing action |
|---|---|
not_connected | Show the “Connect AI budget” button. |
funding_invalid | Same button, with a “reconnect — your funding needs attention” note. |
budget_exceeded | Show usage vs. cap; link them to raise the cap in their dashboard. |
grant_invalid | Show a "Your access grant has been used up" message and offer to request a new one. |
rate_limited | Back off (exponential) or show a brief “slow down” message. |
network / upstream | Retry with backoff; surface a transient-error toast. |
auth | A bug on your side — your secret key or session is wrong. Log and alert. |