Qubittron Bastion
TypeScript SDKAPI reference

Errors

The BastionError hierarchy, HTTP→class mapping, and how to handle each case.

Every error thrown by the SDK is a subclass of BastionError. Branch on instanceof — never on name or message.

BastionError

class BastionError extends Error {
  readonly status: number;            // HTTP status, or 0 for network errors
  readonly code: string | undefined;  // upstream `error.code` if present
  readonly type: string | undefined;  // upstream `error.type` if present
  readonly body: unknown;             // parsed JSON body, or raw string, or cause
}

message defaults to the upstream error.message, falling back to a status-appropriate string ("Invalid API key", "Rate limit exceeded", …) if the body is empty or unparseable.

HTTP → class mapping

HTTP statusClass
400BadRequestError
401AuthenticationError
402, 403PermissionDeniedError
404NotFoundError
429RateLimitError
502, 503, 504UpstreamError
other 4xx/5xxAPIError
network / fetch failureAPIConnectionError (status 0)

All eight classes inherit from BastionError, so a single catch (err) { if (err instanceof BastionError) ... } catches everything API-related.

Class signatures

Most subclasses share BastionError's constructor — they just narrow the type:

type ConstructorOpts = {
  status: number;
  code?: string;
  type?: string;
  body?: unknown;
};

class BadRequestError       extends BastionError { constructor(message: string, opts: ConstructorOpts) }  // 400
class AuthenticationError   extends BastionError { constructor(message: string, opts: ConstructorOpts) }  // 401
class PermissionDeniedError extends BastionError { constructor(message: string, opts: ConstructorOpts) }  // 402, 403
class NotFoundError         extends BastionError { constructor(message: string, opts: ConstructorOpts) }  // 404
class RateLimitError        extends BastionError { constructor(message: string, opts: ConstructorOpts) }  // 429
class UpstreamError         extends BastionError { constructor(message: string, opts: ConstructorOpts) }  // 502, 503, 504
class APIError              extends BastionError { constructor(message: string, opts: ConstructorOpts) }  // catch-all 4xx/5xx

APIConnectionError is shaped differently — it wraps a thrown cause rather than an HTTP response:

class APIConnectionError extends BastionError {
  constructor(message: string, cause?: unknown);
  // status is forced to 0; cause is stored on `body`.
}

So err.status === 0 and err.body === cause for connection errors. Everything else has the HTTP status and a parsed (or raw) response body.

Branching example

import {
  Bastion,
  AuthenticationError,
  BadRequestError,
  PermissionDeniedError,
  RateLimitError,
  UpstreamError,
  APIConnectionError,
  BastionError,
} from "@qubittron/bastion-sdk";

const client = new Bastion();

try {
  await client.chat.completions.create({ model: "x", messages: [] });
} catch (err) {
  if (err instanceof RateLimitError) {
    // exponential backoff and retry
  } else if (err instanceof AuthenticationError) {
    // rotate / refresh credentials
  } else if (err instanceof PermissionDeniedError) {
    // quota exhausted or content blocked — bubble to the user
  } else if (err instanceof BadRequestError) {
    // a bug in our request — log err.body, do not retry
  } else if (err instanceof UpstreamError) {
    // try a different model or back off
  } else if (err instanceof APIConnectionError) {
    // transient network — retry with jitter
  } else if (err instanceof BastionError) {
    // some other 4xx/5xx
  } else {
    throw err; // not from the SDK — rethrow
  }
}

Retry semantics

The SDK does not retry automatically (as of 0.1). Build a thin wrapper on top:

async function withRetry<T>(fn: () => Promise<T>, max = 3): Promise<T> {
  let attempt = 0;
  while (true) {
    try {
      return await fn();
    } catch (err) {
      attempt += 1;
      const retriable =
        err instanceof RateLimitError ||
        err instanceof UpstreamError ||
        err instanceof APIConnectionError;
      if (!retriable || attempt >= max) throw err;
      await new Promise((r) => setTimeout(r, 2 ** attempt * 250 + Math.random() * 250));
    }
  }
}

See error-handling guide for a fuller treatment.

Inspecting the body

The raw parsed body is on err.body. For API errors it follows OpenAI's shape:

{
  "error": {
    "message": "...",
    "type": "invalid_request_error",
    "code": "invalid_api_key"
  }
}

err.code and err.type are populated from this body when present. The raw body field is preserved for forensics — log it with care, the upstream may include the offending request fragment.

On this page