Errors
HTTP status codes, the SDK 0.14 error hierarchy, the 0.13 aliases that still catch, and the structured error payload returned by the Mubit control API.
Every error response carries a flat JSON body with a single human-readable error string:
{
"error": "ingest request exceeds the 1000-item limit"
}There is no nested type/code envelope. The request id travels in the x-request-id response header and a throttled response may carry Retry-After; the SDK copies both onto the exception (request_id, retry_after). Map programmatically on the HTTP status code (below) or, over gRPC, on the canonical status code. The HTTP API is a thin shell over the gRPC service, so every HTTP status is derived from a gRPC code.
Status codes
| Status | gRPC code | Meaning | SDK class | Retried by the SDK? |
|---|---|---|---|---|
400 | InvalidArgument, FailedPrecondition, OutOfRange | Request shape rejected — missing/invalid field, or an input cap exceeded (e.g. >1000 items) | BadRequestError | No |
401 | Unauthenticated | Missing or invalid Authorization header | AuthenticationError | No |
403 | PermissionDenied | Key is valid but not scoped to this run/resource, or the principal is too low for the write | PermissionDeniedError | No |
404 | NotFound | Session, agent, reference id, lesson, job or snapshot does not exist | NotFoundError | No |
409 | AlreadyExists | A unique resource already exists, or a control-plane rule refuses the write (proposer cannot approve own proposal, kill switch is forced on) | AlreadyExistsError (a ConflictError) | No |
410 | — | Endpoint was removed (legacy core SDM lanes); use insert/search instead | GoneError | No |
429 | ResourceExhausted | An upstream dependency (e.g. an LLM provider) is throttling | RateLimitError | Yes, with backoff |
500 and any unlisted 5xx | Internal (and unmapped) | Unhandled server error | InternalServerError | Yes, with backoff |
501 | Unimplemented | The operation does not exist on this server or transport | UnsupportedFeatureError | No |
503 | Unavailable | Backend temporarily unavailable | UnavailableError | Yes, with backoff |
The runtime does not currently emit 422, 499, 502, or 504; the SDK maps 422 to BadRequestError and any other 5xx to InternalServerError should a proxy produce one. Business-rule rejections surface as 400; overload surfaces as 503. See Rate limits for what is and isn't throttled.
The SDK hierarchy
Every class is exported from the top-level package (import mubit / import { ... } from "@mubit-ai/sdk"); there is no mubit.errors submodule in Python (mubit.errors in JS is the same module re-exported).
MubitError
├── ConfigurationError no key, no endpoint, unknown option
├── APIConnectionError DNS, refused, TLS (.endpoint, .refused, .code)
│ ├── APITimeoutError request or connect deadline elapsed
│ └── TransportError 0.13 shape TransportError(code, message); also raised by the gRPC transport
├── APIStatusError the server answered with an error status
│ │ (.status_code, .request_id, .body, .retry_after, .grpc_code, .path)
│ ├── BadRequestError (400) ├── AuthenticationError (401) ├── PermissionDeniedError (403)
│ ├── NotFoundError (404) ├── AlreadyExistsError (409) ├── GoneError (410)
│ ├── RateLimitError (429) ├── InternalServerError (5xx) ├── UnavailableError (503)
│ ├── ControlError /v2/loop control-plane rejection (.status is an alias of .status_code)
│ │ ├── PolicyError policy write rejected (.valid_keys)
│ │ └── ControlUnavailable control plane unreachable; also an APIConnectionError
│ └── LessonRejected a lesson failed the policy schema or its condition (.reason)
└── UnsupportedFeatureError 501, an unknown raw.invoke operation, bidirectional streamingstr(error) is HTTP 404: referenced entry was not found; error.message is the server text alone. request_id is None when the response carried no x-request-id header; retry_after is seconds parsed from Retry-After, else None; grpc_code is set only on the gRPC transport (NOT_FOUND, UNAUTHENTICATED, ...); path is the route the control namespaces called.
Three classes belong to two branches so that the 0.13 handlers below keep catching what they caught:
| Class | Also an instance of | Effect |
|---|---|---|
PermissionDeniedError (403) | AuthenticationError | except AuthError still catches a 403 |
NotFoundError (404) | BadRequestError | except ValidationError still catches a 404 |
AlreadyExistsError (409) | ConflictError and BadRequestError | except ValidationError still catches a 409; catch AlreadyExistsError first to fall back to a get-by-name |
UnavailableError (503) | InternalServerError | except ServerError still catches a 503 and it is retried like any 5xx |
0.13 names
The 0.13 exception names are kept for the whole 1.x line. In Python they are plain aliases or the thin subclasses above:
| 0.13 name | 0.14 | Catches |
|---|---|---|
AuthError | = AuthenticationError | 401, and 403 through PermissionDeniedError |
ValidationError | = BadRequestError | 400/422, and 404, 409 through the subclasses |
AlreadyExistsError | subclass of ConflictError and BadRequestError | 409 |
ServerError | = InternalServerError | 500, any unlisted 5xx, and 503 through UnavailableError; not 410 or 429 |
TransportError | subclass of APIConnectionError | connection failures and gRPC transport codes; .code is UNAVAILABLE | DEADLINE_EXCEEDED | CONNECTION_RESET | IO | UNIMPLEMENTED | CONNECTION_ERROR | TIMEOUT, .refused is True for a refused connection or an unresolvable host |
UnsupportedFeatureError | unchanged | 501 and unknown operations |
GoneError (410) and RateLimitError (429) are new classes with no 0.13 parent: code that only caught ServerError does not see them. Add except mubit.RateLimitError (retry after retry_after) and except mubit.GoneError (the route is gone; change the call) where that matters.
gRPC transport
Over transport="grpc" the same classes are raised from the canonical codes: UNAUTHENTICATED → AuthenticationError, PERMISSION_DENIED → PermissionDeniedError, NOT_FOUND → NotFoundError, ALREADY_EXISTS → AlreadyExistsError, INVALID_ARGUMENT / FAILED_PRECONDITION / OUT_OF_RANGE → BadRequestError, RESOURCE_EXHAUSTED → RateLimitError, INTERNAL / UNKNOWN → InternalServerError. Transport-level codes stay TransportError: UNAVAILABLE, UNIMPLEMENTED, CANCELLED (as IO), a reset connection (CONNECTION_RESET), and DEADLINE_EXCEEDED (also an APITimeoutError). With transport="auto" the client falls back to HTTP for UNAVAILABLE, DEADLINE_EXCEEDED, CONNECTION_RESET, IO and UNIMPLEMENTED before raising.
Python
import time
import mubit
client = mubit.Client()
try:
client.memory.remember("…", kind="fact", run_id=run_id, agent_id="support-agent")
except mubit.AuthenticationError:
raise # fix the API key; a 403 also lands here
except mubit.RateLimitError as e:
time.sleep(e.retry_after or 1.0) # the SDK already retried MUBIT_MAX_RETRIES times
except mubit.BadRequestError as e:
log.error("bad call %s: %s", e.request_id, e.message) # 400, 404, 409: fix the call
except mubit.APIConnectionError as e:
if e.refused: # nothing listening: never retried
raise
... # timeout or reset: already retried; retry the outer job
except mubit.APIStatusError as e:
log.error("HTTP %s on %s: %s", e.status_code, e.path, e.body)The control namespaces (client.policy, client.kill, client.jobs, client.proposals, client.snapshots, client.audit, client.export) raise ControlError for a server rejection; its status property is the 0.13 name of status_code, so except mubit.ControlError as e: if e.status == 403 keeps working. client.policy.set(...) raises PolicyError for an unknown key or a bad value, with valid_keys listing the accepted keys. ControlUnavailable is raised for an unreachable control plane only under on_error="raise"; with "warn" the namespaces log once and return their empty value.
client.memory.remember(..., kind="lesson", lesson_condition=...) raises LessonRejected (a BadRequestError and a ValueError; .reason is the server text) when the lesson does not match the policy's distill.schema or its condition does not parse. It is raised under every on_error setting because the caller asked for the write.
The global helpers (mubit.context, mubit.outcome, mubit.remember, ...) do not raise for server or connection problems under the default on_error="warn"; they return a typed empty value whose reason is one of ok, nothing_to_record, uninitialized, disabled, degraded, legacy, killed, unauthorized, forbidden, unreachable. See SDK configuration → Failure policy.
JavaScript
The same classes are exported from @mubit-ai/sdk, in camelCase: statusCode (with a status alias), requestId, body, retryAfter, path, endpoint, and cause (the underlying fetch or transport error). APIConnectionError carries endpoint, path and code (CONNECTION_ERROR | TIMEOUT).
import { Client, AuthenticationError, RateLimitError, BadRequestError, APIConnectionError, APIStatusError } from "@mubit-ai/sdk";
const client = new Client();
try {
await client.memory.remember("…", { kind: "fact", runId, agentId: "support-agent" });
} catch (err) {
if (err instanceof AuthenticationError) throw err; // 401, and 403
else if (err instanceof RateLimitError) await sleep((err.retryAfter ?? 1) * 1000);
else if (err instanceof BadRequestError) console.error(err.requestId, err.message); // 400, 404, 409
else if (err instanceof APIConnectionError) { if (err.code === "CONNECTION_ERROR") throw err; }
else if (err instanceof APIStatusError) console.error(err.statusCode, err.path, err.body);
}The 0.13 names are subclasses whose instanceof is widened with Symbol.hasInstance, so an existing handler keeps matching what it matched before:
| 0.13 name | Extends | instanceof is true for |
|---|---|---|
ValidationError | BadRequestError | every BadRequestError, NotFoundError (404) and ConflictError (409) |
AuthError | AuthenticationError | every AuthenticationError and PermissionDeniedError (403) |
ServerError | InternalServerError | every APIStatusError with statusCode >= 500 (so UnavailableError too); not GoneError (410) or RateLimitError (429) |
AlreadyExistsError | ConflictError | 409 |
TransportError | APIConnectionError | transport failures; code and refused as in Python |
LessonRejected | ValidationError | a rejected explicit lesson (reason) |
ControlError (an APIStatusError), PolicyError (validKeys) and ControlUnavailable (an APIConnectionError; only with onError: "raise") mirror the Python control-plane classes. A 501 becomes UnsupportedFeatureError, which is a MubitError but not an APIStatusError.
The SDK already retries the retryable rows of the status table; see Retries and idempotency for the exact policy, the environment variables and the manual pattern.