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 status | Class |
|---|---|
| 400 | BadRequestError |
| 401 | AuthenticationError |
| 402, 403 | PermissionDeniedError |
| 404 | NotFoundError |
| 429 | RateLimitError |
| 502, 503, 504 | UpstreamError |
| other 4xx/5xx | APIError |
| network / fetch failure | APIConnectionError (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/5xxAPIConnectionError 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.