The three errors the Ava SDK can throw — APIError, AuthRequiredError, NetworkError — when each is raised, and how to handle them.
The v4 SDK throws three error classes. All three are exported from the package root:
import { APIError, NetworkError, AuthRequiredError } from "@avaprotocol/sdk-js";
For the auto-generated, per-field reference, see the SDK Reference pages linked under each section.
APIError#Thrown when the gateway returns a non-2xx response. The aggregator speaks RFC 7807 problem+json (application/problem+json); APIError surfaces every field of the Problem body plus the HTTP status so you can switch on the failure without re-parsing the body.
| Field | Type | Meaning |
|---|---|---|
status | number | HTTP status code (e.g. 400, 404, 409, 500). |
code? | string | Stable machine-readable code (e.g. WORKFLOW_NOT_FOUND, AUTH_INVALID_TOKEN). |
title? | string | Short human-readable summary (Problem.title) — safe to surface to power users. |
requestId? | string | Per-request identifier — also returned in the X-Request-ID response header. |
problem? | v4.Problem | The full parsed Problem body, when the server returned one. |
message | string (inherited) | The Error.message you see in stack traces — defaults to detail, then title, then HTTP {status} (e.g. HTTP 404). |
When you'll see it: invalid input (4xx), missing entity (404), conflicting state (409), or upstream failures (5xx). Switch on code for stable handling; code is stable across releases, title and the message text aren't.
Recovery: for 4xx, fix the request. For 5xx, retry with backoff — the SDK does not retry server errors automatically; that's your decision.
import { APIError } from "@avaprotocol/sdk-js";
try {
await client.workflows.get(id);
} catch (e) {
if (e instanceof APIError) {
if (e.code === "WORKFLOW_NOT_FOUND") return null; // expected
if (e.status >= 500) throw e; // retry upstream
console.error(`API ${e.status} ${e.code}: ${e.title} (req=${e.requestId})`);
}
throw e;
}
Full class: APIError →
AuthRequiredError#Thrown when a call requires an authenticated client but no valid bearer token is on the transport (or the token has expired).
Default message: "Authentication required — call client.auth.exchange() first".
When you'll see it:
client.auth.clear() and then called something other than health.check().Recovery: re-authenticate, then retry once. The two canonical patterns:
// Production / browser: sign with the user's wallet, then exchange.
import { Client, buildAuthMessage, AuthRequiredError } from "@avaprotocol/sdk-js";
async function ensureAuthed(client: Client, wallet: { signMessage: (m: string) => Promise<string>; address: string }, gatewayVersion: string) {
const { message } = buildAuthMessage({
ownerAddress: wallet.address,
uri: window.location.origin,
chainId: await provider.getNetwork().then(n => Number(n.chainId)),
version: gatewayVersion, // from client.health.check()
});
const signature = await wallet.signMessage(message);
await client.auth.exchange({ ownerAddress: wallet.address, signature, message });
}
// Node tooling: convenience wrapper that signs locally with a key.
await client.auth.exchangeWithKey(process.env.PRIVATE_KEY!, {
uri: "https://app.avaprotocol.org",
chainId: 8453,
version: (await client.health.check()).version,
});
Don't hardcode
uri,chainId, orversion. They're required (no silent defaults) because each one has a real failure mode if it lies — phishing-shaped origin in the wallet popup, JWT routed to the wrong chain, support triage blind to which gateway minted the token. Source them from the runtime: window origin, wallet-reported chain,client.health.check().version.
Full class: AuthRequiredError →
NetworkError#Thrown when the transport itself fails — fetch rejected, the request was aborted (timeout or caller signal), or a 2xx response body wasn't valid JSON.
| Field | Type | Meaning |
|---|---|---|
message | string | fetch <path>: <inner> or Failed to parse JSON response …. |
cause? | unknown | The original error (e.g. the TypeError fetch threw). |
When you'll see it:
fetch throw).AbortController from defaultTimeoutMs, default 30s) or the caller-supplied signal aborted.Recovery: these are typically transient. Retry with exponential backoff, capped attempts, jitter — and surface a "service unavailable" state once retries are exhausted. Don't loop indefinitely.
import { NetworkError } from "@avaprotocol/sdk-js";
try {
await client.workflows.list();
} catch (e) {
if (e instanceof NetworkError) {
// e.cause is the underlying fetch / parse error if you need it
console.warn("transport failed:", e.message, e.cause);
}
throw e;
}
Full class: NetworkError →
caught an error
│
├── instance of AuthRequiredError? → re-auth, then retry once
│
├── instance of NetworkError? → retry with backoff (3–5x cap)
│
├── instance of APIError?
│ ├── status >= 500? → retry with backoff (3–5x cap)
│ ├── status === 409? → reconcile state, optionally retry
│ ├── status === 404? → surface "not found" to caller
│ ├── code === "AUTH_INVALID_TOKEN"? → re-auth, then retry once
│ └── status >= 400 && < 500? → fix request; do NOT auto-retry
│
└── anything else → bug — log and rethrow
When filing bugs, include the requestId from APIError and a timestamp — those are the two pieces that let us correlate against aggregator logs. For NetworkError, include message and cause if you have them.