Error handling
Every API error becomes a typed Python exception. The exception class tells
you the category (so you can branch on retry vs surface vs fix-input);
the .code attribute tells you the specific failure (so you can
display a useful message).
Exception hierarchy
Every SDK exception inherits from ThrustlabError. Only the HTTP-derived
errors inherit from APIError — the other four are siblings of
APIError, not descendants, so except APIError does not catch them:
ThrustlabError # base — catch this for "any SDK error at all"
├── APIError # HTTP error response — carries code/type/request_id
│ ├── AuthenticationError # 401 invalid_api_key
│ ├── ForbiddenError # 403 insufficient permission
│ ├── NotFoundError # 404
│ ├── ValidationError # 400 / 422 with field-level error
│ ├── ConflictError # 409 conflict (e.g. idempotency-key replay)
│ ├── RateLimitError # 429 after retries exhausted
│ └── APIServerError # 5xx after retries exhausted
├── ConfigurationError # SDK misconfigured (no api_key, etc.)
├── NetworkError # transport-level failure (DNS, connect, etc.)
├── SignatureVerificationError # webhook signature mismatch
└── AmbiguousComponentError # .one() / .find() matched more than one component
Because ConfigurationError, NetworkError, SignatureVerificationError,
and AmbiguousComponentError are not APIError subclasses, a bare
except APIError around a call that constructs a Client() or calls
.one() will not catch a bad-config or ambiguous-lookup failure — catch
ThrustlabError if you want one handler for everything.
Import from thrustlab.exceptions:
from thrustlab.exceptions import (
ThrustlabError, APIError, APIServerError, AuthenticationError,
ForbiddenError, NotFoundError, ConflictError, ValidationError,
RateLimitError, NetworkError, SignatureVerificationError,
ConfigurationError, AmbiguousComponentError,
)
Common attributes
Every API exception carries:
| Attribute | Notes |
|---|---|
.code | Stable machine-readable code (invalid_request, card_declined, etc.) |
.type | Error envelope type field (validation_error, rate_limit_error, etc.) |
.message | Human-readable; safe to show to end users |
.request_id | Server-side correlation id; include this in support tickets |
.http_status | HTTP status code that raised |
.param | ValidationError only — the offending body / query field |
.retry_after | RateLimitError only — seconds, may be None |
Branching by category
from thrustlab.exceptions import (
AuthenticationError, ValidationError, RateLimitError,
NotFoundError, APIServerError,
)
try:
sim = client.simulations.create(project_id="proj_xxx", ...)
except ValidationError as exc:
print(f"{exc.code}: {exc.message} (param={exc.param})")
except RateLimitError as exc:
print(f"slow down — retry after {exc.retry_after}s")
except NotFoundError:
print("project doesn't exist")
except AuthenticationError:
print("revoked or invalid API key")
except APIServerError as exc:
print(f"server error {exc.http_status}, request_id={exc.request_id}")
Branching by code
When two different errors share a category but need different handling, use
the code attribute. Codes are stable — adding a new code is non-breaking,
changing one is breaking.
from thrustlab.exceptions import APIError, NotFoundError, ValidationError
try:
client.simulations.create(...)
except ValidationError as exc:
if exc.code == "invalid_component_id":
prompt_user_to_pick_component(exc.param)
else:
raise
except NotFoundError as exc:
if exc.code == "component_not_found":
prompt_user_to_pick_component(exc.param)
else:
raise
except APIError as exc:
# 402 insufficient compute units is NOT in the SDK's status->class map, so it
# surfaces as a bare APIError rather than a dedicated subclass.
if exc.http_status == 402 and exc.code == "credits_insufficient":
prompt_user_to_top_up()
else:
raise
insufficient_credits_error (HTTP 402, code credits_insufficient) has no
dedicated exception class — 402 isn't in the SDK's status-to-class map, so it
always raises the base APIError. Branch on .http_status == 402 (or
.code), not on an exception type.
The full code catalog by resource is in the errors guide.
Catch-all + report
from thrustlab.exceptions import APIError
try:
client.simulations.create(...)
except APIError as exc:
log.warning(
"thrustlab call failed",
extra={
"code": exc.code,
"type": exc.type,
"request_id": exc.request_id,
"http_status": exc.http_status,
},
)
raise
request_id is the most important field to log — it lets ThrustLab support
trace a specific failed request server-side.
Retry exhaustion vs. fail-fast
| Failure | Retried automatically? | Exception class on raise |
|---|---|---|
429 with Retry-After | yes (up to max_retries) | RateLimitError after exhaustion |
| 5xx | yes (up to max_retries) | APIServerError after exhaustion |
| Network error | yes (up to max_retries) | NetworkError after exhaustion |
| 400 / 422 validation | no | ValidationError immediately |
| 401 / 403 / 404 / 409 | no | matching class immediately |
Validation errors fail-fast because retrying won't help — the server's answer won't change for the same body. See retries & timeouts for the retry policy.
Recipe: surface validation errors at the right field
from thrustlab.exceptions import ValidationError
try:
client.projects.create(name="") # too short
except ValidationError as exc:
if exc.param == "name":
print(f"name field error: {exc.message}")
else:
raise
Recipe: handling authentication failures
All API-key failures — missing, malformed, revoked, or belonging to a
deactivated user — collapse to a single AuthenticationError with code
invalid_api_key. This is deliberate anti-enumeration: the server never
tells a caller which of those applies, so the SDK can't either. Don't
branch on a revoked-vs-invalid distinction; treat any AuthenticationError
the same way (prompt for a fresh key):
from thrustlab.exceptions import AuthenticationError
try:
client.users.me()
except AuthenticationError:
prompt_for_new_key()
See also
- Errors guide — full catalog of
codevalues - Retries & timeouts
- Logging & debugging — capturing
request_ids