Skip to main content
Both SDKs are at v1.0.0 and parse every HTTP error into a single, typed exception tree, so you discriminate failures with instanceof (JavaScript) or except (Python) rather than by inspecting status codes by hand. This page is the exhaustive reference for those classes, the automatic retry loop each SDK runs, and the one idempotency behavior that does not work the way the SDK surface suggests. The wire-level error contract — the three HTTP envelope shapes the backend emits and which surface produces each — lives on Errors & status codes. This page covers how the SDKs map that contract onto exception classes. Read both together: the SDKs are more unified than the backend, because each parses all envelope shapes into one class tree.
Every example authenticates with Authorization: Bearer mx_live_… plus the X-Organization-ID header, set once on the client. See Authentication for how those headers are resolved.

How an error reaches your code

Every resource method routes through the SDK’s HTTP engine. On any response with status >= 400, the engine parses the body as JSON (falling back to a {detail} shape built from the response status text when the body is not JSON), then constructs the matching exception class and either retries it or raises it.
1

Parse the body

The engine reads the response body. If it is valid JSON it is kept verbatim as the error body; if not, it becomes `{detail: <status text>}`.
2

Classify by status (and by envelope, for billing)

The status code selects the class. For 402, 403, and 429 the SDK first looks for a structured denial envelope (the flat `{code, layer, key, current, limit, reason}` shape) and, in Python, routes to a BillingError subclass when it finds one.
3

Retry or raise

If the status is retryable and attempts remain, the engine waits and retries. Otherwise it raises the typed exception. See Retry policy.

Error class trees

Base class

Every SDK error subclasses ModulexError. Catch it to handle any API failure uniformly.
number | undefined
The HTTP status. undefined/None for transport errors (StreamError, TimeoutError), which never reach the server.
object
The raw parsed error body. For billing denials this is the flat `{code, layer, key, current, limit, reason}` envelope; for the common case it is `{detail: <string>}`; for validation failures it is `{detail: [{loc, msg, type}, …]}`.
Machine-stable fields lifted from a structured envelope when one is present. In JavaScript these live on the base ModulexError, so every subclass exposes .code, .reason, and .layer. In Python they live on the BillingError family (and code/reason/layer/key/current/limit on its members). All are absent (undefined/None) when the response carried no structured envelope.

Status-mapped subclasses (both SDKs)

Each HTTP status maps to exactly one class. Discriminate by instanceof / except, never by class-name string.
Neither SDK has a dedicated 410 Gone class — both fall through to the base ModulexError. The removed agentic “LLM mode” on POST /workflows/run returns 410; use client.assistant.chat() instead (see the Assistant).

Transport-only classes (no status)

These never carry an HTTP status because they are raised before or outside a server response. Both subclass ModulexError, so a single except ModulexError / instanceof ModulexError still catches them.
On an SSE connect that returns >= 400, both SDKs raise the matching typed exception (for example NotFoundError on a 404, or a BillingError/RateLimitError on a denial) once, before any event is yielded. SSE streams do not auto-reconnect. See SSE run streaming and Streaming & HITL.

RateLimitError extras (429, both SDKs)

A RateLimitError carries four extra fields parsed from the response headers. Each header is omitted by the backend when its value is null, so you may see only some of them.
JavaScript

The BillingError family (Python only)

The Python SDK adds a BillingError tree that the JavaScript SDK does not have. When the backend returns a structured DenialEnvelope on 402/403/429, Python routes it to a subclass based on the envelope’s layer, and exposes the envelope fields as attributes. A BillingError exposes the full envelope: code, layer, key, current, limit, reason, and retry_after (populated only on the 429 layer).
The two SDKs raise different classes for a 402. Python raises PaymentRequiredError/CreditExhaustedError/WalletError (depending on layer); JavaScript has no 402 class and falls through to the base ModulexError, with the envelope fields still on .code/.layer/.reason. Write your JavaScript billing handlers against the base class plus those fields.

The 429 split (Python)

A 429 can arrive in two wire shapes, and Python maps them to two different classes:
  • The header-based rate limit (string detail plus the X-RateLimit-* headers, from the per-key or per-user limiter) raises RateLimitError.
  • A rate-layer denial envelope (the flat structured shape, from the run/managed-usage gate) raises the base BillingError — because BillingError’s layer routing only covers quota/credit/wallet.
So the same 429 status can raise either class depending on which limiter fired. To handle both, catch both:
Python
JavaScript surfaces both 429 shapes as RateLimitError (it discriminates on status, not on envelope shape), so no equivalent split exists there.

Retry policy

Both SDKs retry transient failures automatically. They agree on which statuses are retryable but differ on which HTTP methods are retried.
Python does not retry mutations. A failed POST/PUT/PATCH/DELETE is raised on the first attempt, even for a retryable status. The JavaScript SDK retries all methods, including mutations — so a transient 429/5xx on a mutating call is retried in JavaScript but not in Python. If you depend on automatic retry of a write, do it in JavaScript or implement your own bounded retry in Python.
Configure the retry budget per client:
For the rate limits themselves — which limiters exist and what the 429 responses contain — see Rate limiting. For the per-surface billing gate that produces the 402/403/429 denial envelopes, see Usage gating & limits.

Idempotency

This is the one place where the SDK surface promises more than the backend delivers. The Python executions.run(...) method accepts an idempotency_key= argument, and when you pass it the SDK attaches an Idempotency-Key request header. Its docstring suggests you can “safely retry a run without double-execution,” but the header does not de-duplicate runs.
The Idempotency-Key header does not de-duplicate runs. POST /workflows/run assigns its own run_id on every call, so a caller-supplied Idempotency-Key does not prevent a duplicate: retrying the same run with the same key starts a new, separately-billed run. Do not rely on it to prevent double-execution.
Two consequences for your code:
  • Do not treat run submission as idempotent. If a POST /workflows/run call fails ambiguously (for example a network error after the request may have been received), retrying can execute the workflow twice. Because Python does not auto-retry mutations, this only happens when you retry yourself — so guard those retries with your own application-level dedup (for example, check the run history before resubmitting).
  • The JavaScript SDK does not send the header. There is no idempotencyKey parameter in the JavaScript SDK, consistent with the header not being used for run de-duplication.
Python
For background on the distinct identities a run_id carries (per-turn, per-conversation thread_id, and the durable run row), see Workflows & runs.

Gotchas

Shadowed builtins (Python)

The Python SDK defines PermissionError and TimeoutError, which shadow the Python builtins of the same name. When both the SDK names and the builtins are in scope, except TimeoutError catches the ModuleX one, not asyncio.TimeoutError or builtins.TimeoutError. Import them explicitly and be deliberate about which you catch:
Python

Responses stay snake_case

The SDKs convert your request keys to snake_case on the way out, but they do not convert responses back. Error bodies and their fields (for example has_more, next_cursor, retry_after inside body) stay snake_case as the backend sent them. The typed exception attributes (retryAfter in JavaScript, retry_after in Python) follow each language’s convention, but error.body is the raw wire shape.

Status, class, and retry at a glance

Next: the wire-level error contract

The three HTTP error-envelope shapes, which surface emits each, and the full status taxonomy the SDK classes map onto.