Skip to content

Error codes

The Matrix protocol uses a small, machine-readable error-code catalog so that callers can pattern-match on errors without parsing English. The codes mirror gRPC's Status codes, which keeps the gRPC bridge mechanical.

Status: target state, partial. Many actors today return only { ok: false, error: <message> } without a code field. New code should set code. The catalog below is the convergence target.

The catalog

CodegRPC analogHTTP statusWhen to use
OKOK200success — though usually omitted (just ok: true)
NOT_FOUNDNOT_FOUND404the requested record does not exist
INVALID_ARGUMENTINVALID_ARGUMENT400payload missing required field, malformed shape, validation failure
UNAUTHENTICATEDUNAUTHENTICATED401no session / invalid token
PERMISSION_DENIEDPERMISSION_DENIED403authenticated, but principal not authorized for this op
ALREADY_EXISTSALREADY_EXISTS409unique-key collision on a write
FAILED_PRECONDITIONFAILED_PRECONDITION409op cannot run in the current state (e.g., a Host Service op invoked when the Host is not configured to handle it)
RESOURCE_EXHAUSTEDRESOURCE_EXHAUSTED429rate limit hit, quota exceeded
DEADLINE_EXCEEDEDDEADLINE_EXCEEDED504request timed out
UNAVAILABLEUNAVAILABLE503upstream service unreachable (NATS down, dependency offline)
INTERNALINTERNAL500unexpected internal failure (default for thrown exceptions)

Per-actor declaration

Actors document their error specs through static errorSpecs (MatrixActor.ts:265):

typescript
static errorSpecs = {
  'registry.resolve': [
    { code: 'NOT_FOUND', description: 'No live providers for the logical mount.' },
    { code: 'INVALID_ARGUMENT', description: 'logicalMount missing or empty.' },
  ],
  'registry.register': [
    { code: 'INVALID_ARGUMENT', description: 'logicalMount or providerRuntimeId missing.' },
  ],
};

$introspect { depth: 'full' } returns this; protocol gateways surface it (gRPC error details, OpenAPI responses, MCP error annotations).

Examples from running code

Reply textSuggested code
'gateway.http.resolve requires a non-empty pathname'INVALID_ARGUMENT
'No Host runtime webapp route matched the request'NOT_FOUND
'gateway.http.request only supports GET and HEAD'INVALID_ARGUMENT (HTTP 405)
'runtimes.reconcile is not available in daemonless Host mode' (legacy literal; the retired matrixd path is disabled — Host Service is the supported path)FAILED_PRECONDITION
'request timed out after 5000ms' (transport-side)DEADLINE_EXCEEDED

These are the patterns. Today the actors emit only the error text; adding the code is a per-package backfill.

Code vs HTTP status

When the op is exposed via the HTTP gateway, the statusCode field on the error payload sets the HTTP response status. The code field is internal/structural; the statusCode is the HTTP-mapping. Both are optional; consistent code with the table above keeps them in sync.

Code vs message

  • code: stable across versions, machine-matchable, never localized.
  • error: human-readable, may evolve message text without breaking callers, may include op-specific values ("expected symbol, got ''").
  • details: structured op-specific data the caller can use programmatically (e.g., the field name that failed validation).

What code is NOT

  • It is not free-form. Callers pattern-match on the catalog above. Custom codes belong in details.subcode if needed.
  • It is not localized. Pass localized text in error; keep code ASCII-uppercase.
  • It is not authoritative for HTTP status. Set statusCode explicitly if your op maps to HTTP.

Adding a new code

The catalog is intentionally small. Before introducing a new top-level code:

  1. Check the gRPC Status codes — if a match exists, use it.
  2. If the situation is op-specific (e.g., "wrong git branch", "stale Device link"), use the closest catalog code and put the specific shape in details.
  3. If a truly new code is warranted, propose it in WORKSTREAMS/core-and-packaging/ and reference this page.

Convergence on the small catalog is the goal — every new code raises the cost for callers and gateway adapters.

See also