qualiaDocs

SDK

Errors, retries, and timeouts

How the SDK reports failures, which ones to retry, and how to set deadlines.

This page shows how @qualiaso/sdk reports API errors and network failures, gives you a retry helper, and shows how to set timeouts. For what each error code means, see Handle errors and rate limits.

What returns and what throws

FailureWhat the SDK does
The API returns an error status, such as 401 or 422Returns it in error. Does not throw.
The request never gets a response, such as a network failure or DNS errorThrows.
The request times out through an AbortSignalThrows.

error holds the parsed error body, so the code and message are one level down, in error.error:

{
  "error": {
    "code": "unauthorized",
    "message": "Invalid API key",
    "requestId": "req_x2f9kq7wz1mv8c3jtr0hbnyd"
  }
}

Branch on the status

response is set on every result. Branch on response.status, which works with every SDK version:

const { data, error, response } = await qualia.createActor({
  body: { email: "anna@ridgeline.co" },
});

if (error) {
  switch (response.status) {
    case 401:
      throw new Error("Qualia rejected the API key. Check QUALIA_API_KEY.");
    case 402:
      // Out of credits. Queue the signup and send it after you add credits.
      break;
    case 422:
      // The message names the invalid field.
      console.error(error.error.message);
      break;
    case 429:
      // Wait for Retry-After seconds, then retry. See the helper below.
      break;
    default:
      console.error(`Qualia returned ${response.status}`, error);
  }
} else {
  console.log(data.actorId);
}

Throw on errors instead

Pass throwOnError: true to make a call throw on any error status. The thrown value is the parsed error body. It is a plain object without a stack trace:

try {
  const { data } = await qualia.createActor({
    body: { email: "anna@ridgeline.co" },
    throwOnError: true,
  });
  console.log(data.actorId);
} catch (err) {
  // An error body such as { error: { code, message, requestId } },
  // or an Error from a network failure or timeout.
  console.error(err);
}

Retry transient failures

Retry 429 responses, 5xx responses, and network failures. Do not retry other 4xx responses, because the same request gets the same error. Retrying createActor is safe: each email maps to one actor.

This helper wraps any SDK call. It waits for Retry-After on a 429, and backs off 1, 2, then 4 seconds otherwise:

lib/with-retry.ts
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

export async function withRetry<T extends { response: Response }>(
  call: () => Promise<T>,
  maxAttempts = 4,
): Promise<T> {
  for (let attempt = 1; ; attempt++) {
    const backoffMs = 1000 * 2 ** (attempt - 1);

    let result: T;
    try {
      result = await call();
    } catch (err) {
      // Network failure or timeout.
      if (attempt >= maxAttempts) throw err;
      await sleep(backoffMs);
      continue;
    }

    const { status, headers } = result.response;
    const retryable = status === 429 || status >= 500;
    if (!retryable || attempt >= maxAttempts) return result;

    const retryAfterSeconds = Number(headers.get("Retry-After"));
    await sleep(retryAfterSeconds > 0 ? retryAfterSeconds * 1000 : backoffMs);
  }
}

Use it like this:

const { data, error, response } = await withRetry(() =>
  qualia.createActor({ body: { email: "anna@ridgeline.co" } }),
);

Set a timeout

The SDK uses fetch, so it accepts an AbortSignal. Pass signal to give one call a deadline:

const { data, error } = await qualia.createActor({
  body: { email: "anna@ridgeline.co" },
  signal: AbortSignal.timeout(5_000),
});

When the deadline passes, the call throws a TimeoutError.

To give every call the same deadline, pass a custom fetch when you create the client. This replaces any signal you pass to a single call:

lib/qualia.ts
import { Qualia } from "@qualiaso/sdk";

export const qualia = new Qualia({
  auth: () => process.env.QUALIA_API_KEY!,
  fetch: (input, init) =>
    fetch(input, { ...init, signal: AbortSignal.timeout(10_000) }),
});

Next steps