Error Handling

Handle SDK errors gracefully — error classes, status codes, retry patterns, and best practices for production use.

3 min read
Share

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.

typescript
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.

typescript
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.

CodeHTTP StatusCause
NETWORK_ERRORfetch failed: DNS failure, connection refused, timeout, or offline
PARSE_ERRORvariesAPI returned a non-JSON body (e.g. HTML error page)
HTTP_ERROR4xx / 5xxGeneric HTTP error without a structured body
API_ERROR4xx / 5xxStructured error from the API: { error: { code, message, details? } }
STREAM_ERRORSSE 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)

typescript
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)

typescript
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.

typescript
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)

typescript
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)

typescript
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.

typescript
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

typescript
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).

typescript
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

typescript
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.

typescript
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.

typescript
// 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.

typescript
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

SituationCheck
Is it an SDK error?error instanceof AskVerdictError
Network / offlineerror.code === "NETWORK_ERROR"
Invalid API keyerror.status === 401
Forbidden actionerror.status === 403
Resource not founderror.status === 404
Validation failureerror.status === 422
No creditserror.status === 402 || error.code === "INSUFFICIENT_CREDITS"
Rate limitederror.status === 429
Server errorerror.status !== undefined && error.status >= 500
SSE stream failederror.code === "STREAM_ERROR"
Should retry?Network, 429, or 5xx
Should NOT retry?4xx (except 429)

Was this page helpful?