Errors & retries

Typed errors

A non-2xx response maps to a typed error carrying the HTTP status, the API's error code (integer), the message, and the request URL:

HTTP Error type
400 / 422 ValidationError
401 UnauthorizedError
403 ForbiddenError
404 NotFoundError
409 ConflictError
429 RateLimitError
5xx ServerError

All extend a common base (BoostSpaceApiError / APIError), so you can catch broadly or narrowly:

from boostspace import NotFoundError, BoostSpaceApiError
try:
    bs.spaces.ref(999999).get()
except NotFoundError:
    ...
except BoostSpaceApiError as e:
    print(e.http_status, e.code, e.message)
try { await bs.spaces.ref(999999).get(); }
catch (e) { if (e instanceof NotFoundError) { /* … */ } }
_, err := bs.Spaces.Ref(999999).Get(ctx)
var nf *boostspace.NotFoundError
if errors.As(err, &nf) { /* … */ }

> The error code is an integer — the HTTP status for generic errors, or a > specific internal code (e.g. 4002 missing field, 4005 not found, 4031 2FA > required) for finer cases. Don't compare it to strings.

Retries

The runtime retries automatically, but only when it is safe:

  • 429 — always retried (rejected before processing), honouring Retry-After.
  • 5xx / network errors — retried only for idempotent methods (GET/PUT/DELETE/HEAD/OPTIONS). A POST is never retried, so you can't get a duplicate create.
  • Backoff is exponential with jitter; maxRetries defaults to 3 and is configurable at construction.

Observability hooks (onRequest/onResponse/onError) fire once per logical request (retries are internal) and can inject headers (e.g. trace context).