Error handling
Error classes, retry semantics, and idempotency.
Every non-2xx response is mapped to a typed exception that subclasses
easylabs.EasyError. Catch the specific subclass for targeted handling
or EasyError to catch everything the SDK throws.
from easylabs import (
EasyError, # base class for every SDK error
AuthenticationError, # 401
PermissionError, # 403 — note: shadows the Python builtin
NotFoundError, # 404
ConflictError, # 409
InvalidRequestError, # 400 / 422
RateLimitError, # 429
ServerError, # 5xx
)
easylabs.PermissionErrorshadows the Python built-inPermissionError. Always import explicitly fromeasylabs(or reference aseasylabs.PermissionError) to keep things unambiguous.
Every EasyError carries the same fields:
| Attribute | Type | Notes |
|---|---|---|
message | str | Human-readable summary. |
status | int | HTTP status code. |
code | str | None | Server-supplied error code (e.g. "INVALID_CARD"). |
details | Any | Field-level details from the API. |
retry_after_seconds | int | None | Parsed from the Retry-After header on 429s. |
raw | Any | The raw decoded JSON body (or None). |
Retryable vs. non-retryable
| HTTP status | Exception | Retry? |
|---|---|---|
| 400 | InvalidRequestError | No — fix the request. |
| 401 | AuthenticationError | No — fix the key. |
| 403 | PermissionError | No — caller lacks the required scope. |
| 404 | NotFoundError | No — the ID doesn't exist (or never did). |
| 409 | ConflictError | No — re-read state, then retry deliberately. |
| 422 | InvalidRequestError | No — validation failed. |
| 429 | RateLimitError | Yes — wait retry_after_seconds. |
| 5xx | ServerError | Yes — exponential backoff with jitter. |
A minimal retry loop:
import time
from easylabs import RateLimitError, ServerError
def with_retries(fn, *, attempts: int = 5):
for i in range(attempts):
try:
return fn()
except RateLimitError as e:
time.sleep(e.retry_after_seconds or 1)
except ServerError:
time.sleep(min(2 ** i, 30))
return fn() # last attempt; let the exception escape if it failsIdempotency keys
Every mutating method on every resource accepts an idempotency_key=
keyword argument. The SDK sends it as the Idempotency-Key header.
Replays of the same key within the server's retention window return
the original response — safe to retry without creating duplicate
charges, customers, or refunds.
import uuid
key = f"refund-{order_id}-{uuid.uuid4()}"
client.transfers.create_refund(
transfer_id,
refund_amount=2500,
idempotency_key=key,
)A few rules of thumb:
- Generate the key once per logical operation (e.g. one per checkout attempt), then reuse it across retries.
- Use UUIDs or any unique string up to 255 characters.
- Read endpoints (
list,retrieve) accept the header but ignore it — no harm in passing it through unconditionally if your transport middleware does.
The SDK does not retry automatically. Pair idempotency_key= with the
retry loop above to make the combination safe.
Network errors
Network-level failures are not wrapped — httpx raises them directly.
You'll see things like:
httpx.ConnectError— DNS / TCP failure.httpx.ReadTimeout/httpx.WriteTimeout— request exceeded the default 30s timeout.httpx.RemoteProtocolError— malformed server response.
Handle them alongside EasyError if you need a single retry layer:
import httpx
from easylabs import EasyError
try:
customer = client.customers.retrieve("cus_123")
except (httpx.HTTPError, EasyError) as e:
log.warning("Easy API call failed: %s", e)
raise