Error Handling
Handle SDK errors gracefully — error classes, status codes, retry patterns, and best practices for production use.
Every method on AskVerdictClient throws an AskVerdictError when the API returns a non-2xx response or a network-level failure occurs. Understanding the error shape lets you write precise, recoverable error handling instead of swallowing exceptions.
The AskVerdictError Class
AskVerdictError extends the native Error class with three extra properties.
import { AskVerdictError } from "@askverdict/sdk";
class AskVerdictError extends Error {
readonly name: "AskVerdictError";
readonly code: string; // Machine-readable error code
readonly status: number | undefined; // HTTP status (undefined for network errors)
readonly details: Record<string, unknown> | undefined; // Optional extra context
}Checking for SDK errors
Always use instanceof AskVerdictError before accessing .code or .status. This avoids accidentally treating unexpected runtime errors (like TypeError) as API errors.
import { AskVerdictClient, AskVerdictError } from "@askverdict/sdk";
const client = new AskVerdictClient({ apiKey: process.env.ASKVERDICT_API_KEY });
try {
const result = await client.createVerdict({
question: "Should we rewrite the backend in Rust?",
mode: "thorough",
});
} catch (error) {
if (error instanceof AskVerdictError) {
// SDK error — structured, recoverable
console.error(`[${error.code}] ${error.message}`);
console.error(`HTTP status: ${error.status ?? "N/A (network error)"}`);
if (error.details) {
console.error("Details:", error.details);
}
} else {
// Unexpected runtime error — re-throw
throw error;
}
}Error Codes Reference
The SDK normalizes all failures to one of five error codes.
| Code | HTTP Status | Cause |
|---|---|---|
NETWORK_ERROR | — | fetch failed: DNS failure, connection refused, timeout, or offline |
PARSE_ERROR | varies | API returned a non-JSON body (e.g. HTML error page) |
HTTP_ERROR | 4xx / 5xx | Generic HTTP error without a structured body |
API_ERROR | 4xx / 5xx | Structured error from the API: { error: { code, message, details? } } |
STREAM_ERROR | — | SSE stream failed to open, or response body was empty |
API_ERROR vs HTTP_ERROR
API_ERROR means the server returned a well-formed error envelope { error: { code, message } }. The .code field will contain the server-provided code (e.g. "INSUFFICIENT_CREDITS" or "DEBATE_NOT_FOUND"). HTTP_ERROR is the fallback when the response body could not be parsed.
Handling Specific Error Types
Authentication errors (401)
try {
const me = await client.getMe();
} catch (error) {
if (error instanceof AskVerdictError && error.status === 401) {
// API key is invalid, expired, or missing
console.error("Invalid API key. Generate a new one at askverdict.ai/developer.");
process.exit(1);
}
throw error;
}Authorization errors (403)
try {
await client.deleteVerdict("dbt_other_users_debate");
} catch (error) {
if (error instanceof AskVerdictError && error.status === 403) {
console.error("You do not have permission to delete this verdict.");
} else {
throw error;
}
}Validation errors (422)
The API returns field-level validation errors in error.details. Inspect them to show user-friendly messages.
try {
await client.createVerdict({ question: "" }); // Empty question
} catch (error) {
if (error instanceof AskVerdictError && error.status === 422) {
console.error("Validation failed:", error.message);
// error.details may contain field-level errors, e.g.:
// { "question": ["Question must be at least 10 characters"] }
if (error.details) {
for (const [field, messages] of Object.entries(error.details)) {
console.error(` ${field}: ${JSON.stringify(messages)}`);
}
}
} else {
throw error;
}
}Not found errors (404)
try {
const { verdict } = await client.getVerdict("dbt_does_not_exist");
} catch (error) {
if (error instanceof AskVerdictError && error.status === 404) {
console.error("Verdict not found. It may have been deleted.");
} else {
throw error;
}
}Insufficient credits (402)
try {
await client.createVerdict({ question: "...", mode: "thorough" });
} catch (error) {
if (
error instanceof AskVerdictError &&
(error.status === 402 || error.code === "INSUFFICIENT_CREDITS")
) {
const balance = await client.getBalance();
console.error(
`Not enough credits. You have ${balance.creditBalance} credits. ` +
`"thorough" mode costs 8 credits. Top up at askverdict.ai/billing.`
);
} else {
throw error;
}
}Network errors
Network errors have no HTTP status. Detect them by code.
try {
await client.health();
} catch (error) {
if (error instanceof AskVerdictError && error.code === "NETWORK_ERROR") {
console.error("Could not reach the AskVerdict API. Check your internet connection.");
} else {
throw error;
}
}Stream errors
try {
for await (const event of client.streamVerdict("dbt_abc123")) {
console.log(event.type);
}
} catch (error) {
if (error instanceof AskVerdictError && error.code === "STREAM_ERROR") {
console.error("SSE stream failed to open. The debate may have already completed.");
} else {
throw error;
}
}Rate Limiting (429)
The API enforces per-key rate limits. A 429 response means you have exceeded the allowed request rate. Implement exponential backoff to handle this automatically.
Do not retry immediately on 429. Rapid retries will keep triggering the rate limit. Always wait before the next attempt and honor Retry-After headers when present.
Retry with Exponential Backoff
For production applications, wrap SDK calls with retry logic. The pattern below retries on network errors, 429 Too Many Requests, and 5xx server errors — but gives up immediately on 4xx client errors (which indicate a problem with your request, not a transient failure).
import { AskVerdictClient, AskVerdictError } from "@askverdict/sdk";
interface RetryOptions {
maxAttempts?: number;
initialDelayMs?: number;
maxDelayMs?: number;
jitter?: boolean;
}
async function withRetry<T>(
fn: () => Promise<T>,
opts: RetryOptions = {},
): Promise<T> {
const {
maxAttempts = 3,
initialDelayMs = 500,
maxDelayMs = 10_000,
jitter = true,
} = opts;
let lastError: unknown;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (!(error instanceof AskVerdictError)) {
throw error; // Not an SDK error — don't retry
}
const isRetryable =
error.code === "NETWORK_ERROR" ||
error.status === 429 ||
(error.status !== undefined && error.status >= 500);
if (!isRetryable || attempt === maxAttempts - 1) {
throw error;
}
// Exponential backoff: 500ms, 1000ms, 2000ms, …
let delay = Math.min(initialDelayMs * 2 ** attempt, maxDelayMs);
// Add random jitter to avoid thundering herd
if (jitter) {
delay = delay * (0.5 + Math.random() * 0.5);
}
console.warn(
`[AskVerdict] Attempt ${attempt + 1} failed (${error.code}). ` +
`Retrying in ${Math.round(delay)}ms…`,
);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw lastError;
}Using the retry wrapper
const client = new AskVerdictClient({ apiKey: process.env.ASKVERDICT_API_KEY });
// Single call with retry
const result = await withRetry(
() => client.createVerdict({ question: "...", mode: "balanced" }),
{ maxAttempts: 3, initialDelayMs: 500 },
);
// Polling with retry
async function getCompletedVerdict(id: string) {
while (true) {
const { verdict } = await withRetry(() => client.getVerdict(id));
if (verdict.status === "completed") return verdict;
if (verdict.status === "failed") throw new Error(`Debate ${id} failed`);
await new Promise((r) => setTimeout(r, 3_000));
}
}Stream Reconnection
streamVerdict does not auto-reconnect on drop. For long-running debates, wrap the stream in a retry loop that resumes on NETWORK_ERROR or STREAM_ERROR.
async function* reliableStream(
client: AskVerdictClient,
id: string,
maxRetries = 3,
): AsyncGenerator<StreamEvent> {
let retries = 0;
while (retries <= maxRetries) {
try {
for await (const event of client.streamVerdict(id)) {
yield event;
if (event.type === "stream:end" || event.type === "debate:complete") {
return; // Normal completion — stop retrying
}
}
return; // Generator exhausted normally
} catch (error) {
if (
error instanceof AskVerdictError &&
(error.code === "NETWORK_ERROR" || error.code === "STREAM_ERROR") &&
retries < maxRetries
) {
retries++;
const delay = 1_000 * retries;
console.warn(`Stream disconnected. Reconnecting in ${delay}ms… (attempt ${retries})`);
await new Promise((r) => setTimeout(r, delay));
} else {
throw error;
}
}
}
}
// Usage
for await (const event of reliableStream(client, "dbt_abc123")) {
console.log(event.type, event.data);
}Error Handling in Different Environments
React / Next.js
In UI code, catch errors at the component boundary and translate them to user-facing messages.
// hooks/use-create-verdict.ts
import { useState } from "react";
import { AskVerdictError } from "@askverdict/sdk";
import { client } from "@/lib/sdk-client";
export function useCreateVerdict() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
async function create(question: string) {
setLoading(true);
setError(null);
try {
const result = await client.createVerdict({ question, mode: "balanced" });
return result;
} catch (err) {
if (err instanceof AskVerdictError) {
switch (err.status) {
case 401:
setError("Your session has expired. Please sign in again.");
break;
case 402:
setError("Not enough credits. Top up your balance to continue.");
break;
case 422:
setError(err.message);
break;
case 429:
setError("Too many requests. Please wait a moment and try again.");
break;
default:
setError("Something went wrong. Please try again.");
}
} else {
setError("An unexpected error occurred.");
console.error(err);
}
return null;
} finally {
setLoading(false);
}
}
return { create, loading, error };
}Node.js scripts
For non-interactive scripts, log the full error and exit with a non-zero code.
import { AskVerdictClient, AskVerdictError } from "@askverdict/sdk";
const client = new AskVerdictClient({ apiKey: process.env.ASKVERDICT_API_KEY! });
async function run() {
try {
const { verdict } = await client.getVerdict(process.argv[2]!);
console.log(JSON.stringify(verdict, null, 2));
} catch (error) {
if (error instanceof AskVerdictError) {
console.error(`Error [${error.code}]: ${error.message}`);
process.exit(1);
}
throw error;
}
}
run();Quick Reference
| Situation | Check |
|---|---|
| Is it an SDK error? | error instanceof AskVerdictError |
| Network / offline | error.code === "NETWORK_ERROR" |
| Invalid API key | error.status === 401 |
| Forbidden action | error.status === 403 |
| Resource not found | error.status === 404 |
| Validation failure | error.status === 422 |
| No credits | error.status === 402 || error.code === "INSUFFICIENT_CREDITS" |
| Rate limited | error.status === 429 |
| Server error | error.status !== undefined && error.status >= 500 |
| SSE stream failed | error.code === "STREAM_ERROR" |
| Should retry? | Network, 429, or 5xx |
| Should NOT retry? | 4xx (except 429) |
Was this page helpful?