Data Connectors

Data Connector Errors

Reference for the error classes a data connector extension throws, their codes, and how Engine treats and reports each one.
Preview
Extensions are in preview and can change in breaking ways between releases. Use them in non-production environments only. See Stability. Written for Monospace 1.0.0, @monospace/cli 0.2 and @monospace/extension-kit 0.3.

Throw a Connector Error

A data connector extension reports a failure by throwing one of the kit's error classes from setup or a handler. Import them from @monospace/extension-kit/data-connector.

The connector exposes an external data source as collections, which are containers for items of the same type. An item is a single data object, and its named values are fields. This excerpt from the Stripe example's client throws a kit class when the Stripe API can't be reached or doesn't answer in time. It reads the body inside the same try, and keeps the rejection as cause; // … marks the lines it leaves out:

  • A permission denial survives the wrap. A fetch to a host outside the data source's granted permissions rejects with an error named NotCapable. Engine looks for it anywhere in the thrown error's cause chain, so wrapped with { cause: error } it's still reported as invalid permissions (2003), with your message nested inside. Wrapped without cause, it reads as the wrapping class. See Handle a Denied Request.
  • The deadline covers the body. AbortSignal.timeout also aborts a body read, so the read sits inside the try. A timeout rejects with an error named TimeoutError.
  • A body that isn't JSON becomes a plain Error, with a message the caller can read, not a SyntaxError from deep inside the parser. No class fits a changed API, and Engine reports the plain Error as a failed query.

Handle Data Connector Errors walks through how the Stripe client maps each error status to a class.

Every class takes (message: string, options?: { cause?: unknown }). The message is what Engine reports, so write it for the person reading the error, not for yourself. cause stays on the JavaScript error, where Engine looks only for a permission denial. Engine receives only the code and the message (see Know What the Caller Sees).

Engine recognizes an error by its monospace property, { code }, not by its class. Two bundled copies of the kit, or a connector without the kit, work the same way: a thrown object whose monospace.code is a safe integer is read identically. Use the kit's codes. Engine reports a code it doesn't recognize as a failed query.

Pick an Error Class

ClassCodeThrow when
QueryRejected100A valid query or write can't be served as asked (e.g., without a filter the external data source requires, beyond a page window or a read budget, or with a value outside a field's rule). The caller can change the request, so write the message for them
UniqueConstraintViolation1002The external data source rejects a write because a value must be unique
NullConstraintViolation1003The external data source rejects a write because a required value is missing
ForeignKeyConstraintViolation1004The external data source rejects a write because a referenced item doesn't exist, or another item still references it
InvalidConfiguration2001The data source's configuration is missing a value or has a wrong one, found before or without asking the external data source (e.g., a malformed URL or an unknown region)
AccessDenied2002The external data source refuses this account: its credentials are rejected, expired or revoked, lack a scope, can't reach the resource, or exceed the plan
CollectionNotFound2004A collection your connector declared no longer exists in the external data source (e.g., an object type deleted after the data source was introspected). Engine sends only declared collections
ConnectionFailed5001The external data source can't be reached, or won't answer for now: a network failure, a timeout, maintenance, or a server error it reports as temporary
RateLimited5002The external data source is throttling the connector: a rate limit, or a quota that resets. For a limit only a plan change lifts, throw AccessDenied

No class reports a missing item. A keyed operation (readOne, updateOne, deleteOne) whose key or guard misses answers { record: null }, which callers see as 404. A read that matches nothing answers no items. See Keyed Operations.

Each code's range says who can fix the failure: 1 to 999 is invalid input and 1000 to 1999 a failure the caller can fix. 2000 to 2999 needs someone with access to the data source or to the external data source's account, and 5000 and up is infrastructure, where a later retry may work.

All nine classes extend the abstract ConnectorError. The module also exports the ErrorCode map, and the ErrorCodeName, ShellErrorCodeName, ThrownErrorCode, ThrownErrorCodeName, ConnectorErrorOptions and ConnectorErrorBrand types.

For anything no class describes, throw a plain Error. Engine reports it as a failed query. The example connectors do this in two cases: a changed API (a successful answer that isn't JSON, has the wrong shape, or holds a value of the wrong type), and a request their declaration never offered. See Tell QueryRejected from a Plain Error.

Read Codes from ErrorCode

ErrorCode maps each name to its code, for code that recognizes an error without instanceof, such as a smoke test that drives the built file. Compare with its entries, never with hard-coded numbers. The kit generates it from Engine's own list of codes:

@monospace/extension-kit/data-connector
export const ErrorCode = {
    QueryRejected: 100,
    UniqueConstraintViolation: 1002,
    NullConstraintViolation: 1003,
    ForeignKeyConstraintViolation: 1004,
    InvalidConfiguration: 2001,
    AccessDenied: 2002,
    InvalidPermissions: 2003,
    CollectionNotFound: 2004,
    ConnectionFailed: 5001,
    RateLimited: 5002,
    MethodNotFound: -32_601,
} as const;

Only Engine's side of the protocol raises InvalidPermissions (a permission denial) and MethodNotFound (a missing handler). The type ShellErrorCodeName names them. The kit has no class for either, and a class's monospace.code is typed to the nine codes a connector throws. Engine reports a code it doesn't recognize as a failed query: 500 "Failed to query the database".

Tell QueryRejected from a Plain Error

Both refuse a request before or instead of answering it. Pick by whose request it is:

  • QueryRejected: the request fits the declaration, but the external data source can't serve it as asked. That includes shapes Engine composes on its own, whatever the collection declares: not around an update's field rules, and or across permission rules. See Handle Requests You Didn't Declare. The caller gets 422.
  • A plain Error: the request names something the collection never declared. That's an unknown collection or namespace, an operation, a filter field or operator, a sort, a count, a key other than the declared one, or a write. Nothing the caller changes fixes it, so don't use QueryRejected, which tells the caller to change the request. The caller gets 500 "Failed to query the database".

Engine checks callers' requests against the data source's stored schema, but not the filters it sends to your connector. So a comparison outside your declaration can come from a permission rule's condition or an extension update that narrowed the declaration without a schema refresh. Refuse it rather than drop it: a dropped condition can widen what the caller may touch.

Some undeclared comparisons are expected: Engine's relation and read-back lookups on a key no declaration can carry. Serve them. See Handle Requests You Didn't Declare.

Use InvalidConfiguration and AccessDenied for Configuration

Where you throw them decides what happens:

  • From setup. Engine stops the data source until the extension is reloaded, because a retry can't fix the configuration. The request that started the connector and every later one that reaches it answer 422 "Invalid extension configuration", with "the extension is deactivated" and your message nested inside; results the cache already holds are still served.
  • From preflight. It also stops the data source until the extension is reloaded. Throw it there when the configuration preflight reads to derive hosts is invalid.
  • From query or testConnection (e.g., AccessDenied for a 401 from the external data source). Engine fails that request only, with 422 "Invalid extension configuration". The data source keeps running.

Throw InvalidConfiguration for a value you can tell is wrong without a request, and AccessDenied for credentials the external data source refuses. For a request your connector can't serve, throw QueryRejected: either configuration class tells the caller to check the configuration, which isn't the problem.

Know What Deactivates a Data Source

A failure in setup or preflight stops the data source until the extension is reloaded only when a retry can't fix it:

Thrown from setup or preflightEffect
InvalidConfiguration or AccessDeniedDeactivates the data source
A permission denial, raw or anywhere in the cause chainDeactivates the data source. From a handler, the same denial fails only that request, with 422
setup returning no handler object, or handlers without introspect or queryDeactivates the data source
Any other class, or a plain ErrorFails that request with 422 "Invalid extension configuration". The next request tries again

Nothing thrown from introspect, query or testConnection deactivates a data source. A deactivated data source answers every request that reaches its connector, including the one whose start deactivated it, with the error that deactivated it, and "the extension is deactivated" nested inside:

ReasonCaller's HTTP status and message
setup or preflight threw InvalidConfiguration or AccessDenied, or setup returned no usable handlers422 "Invalid extension configuration"
A permission denial in setup or preflight422 "Invalid extension permissions"
The grant doesn't match the permissions the connector requires422 "Invalid extension permissions", with code 7001 and the required set in meta.permissions. See Change Permissions
The connector file fails to load, preflight answers something the protocol doesn't define, the entry's extension is no longer installed, or its query capabilities changed503 "Data source is deactivated". See When the Capabilities Change

Know the Defaults for Unbranded Throws

A thrown value without a monospace property gets a code from what it carries:

Thrown valueReported as
A value with a permission denial anywhere in its cause chain (e.g., a fetch to a host outside the data source's granted permissions), from setup, preflight or a handlerInvalid permissions (2003). This wins over any monospace property. See Network Permissions
Anything else, from a handler (introspect, query, testConnection)Internal error (-32603), a failed query
Anything else, from setup or preflight422 "Invalid extension configuration". It doesn't deactivate the data source

A monospace property that is set but has no safe-integer code is reported as an internal error (-32603), wherever it was thrown. A falsy monospace property (e.g., null) counts as none.

Throw a kit class instead of letting a raw error escape, so Engine can tell a rate limit from a rejected credential or an outage.

Keep Secrets out of Messages

Only the code and the message cross to Engine, and the message reaches the caller in the HTTP error response. Nothing else about the error does: not its cause, its stack, or any other property.

  • Never interpolate a credential (a key, token, or request header) into a message. When the external data source's own message echoes a credential, replace it. The Stripe client throws AccessDenied with a fixed message, "Stripe rejected the API key (HTTP 401).", because Stripe's message echoes the key's last characters.
  • Leave out URLs that identify an account, such as Stripe's request_log_url, and never copy a whole response body into a message.
  • Leave out the query string of a request you describe. It can hold the caller's filter values. The Stripe client names the path only: "Stripe answered HTTP 500 to GET /v1/customers."
  • Leave out personal data from items, such as a customer's email.

See Keep Secrets out of Messages for the Stripe example.

Know What the Caller Sees

A caller is whoever sent the request to the Monospace API, from an SDK, REST, or Studio. Engine translates each code into its own error before answering. On the REST API, a failed query answers with "message": "Query failed". Engine's message for the code is nested inside it, and your message under "the extension returned an error:":

ClassEngine's messageCaller's HTTP status
CollectionNotFoundTable does not exist404
UniqueConstraintViolationUnique constraint failed422
NullConstraintViolationNull constraint failed422
ForeignKeyConstraintViolationForeign key constraint failed422
QueryRejectedThe data source rejected the query422
A permission denial (2003)Invalid extension permissions422
ConnectionFailedFailed to connect to the database500
RateLimitedFailed to connect to the database500, not 429, and without a Retry-After header
InvalidConfigurationInvalid extension configuration422
AccessDeniedInvalid extension configuration422
A missing handler (-32601)Unsupported operation: a protocol method the extension does not implement500
A plain Error, or a code Engine doesn't recognizeFailed to query the database500

The Art Institute connector's refusal of a page past its 1,000-result window answers this way:

response.json
{
  "message": "Query failed",
  "source": {
    "message": "The data source rejected the query",
    "source": {
      "message": "the extension returned an error: The Art Institute search API returns only the first 1000 results. Narrow the filter or read an earlier page."
    }
  }
}

The table covers errors your handlers throw. A request that arrives while another request starts or replaces the connector answers 503 "Data source is restarting".

A connection test (POST /api/{workspace}/sources/data/test) wraps the messages of a failing testConnection in "Failed to connect to data source with the provided configuration" instead of "Query failed". A failure while the connector starts, such as an error thrown from setup, reads "The extension could not be started" instead.

These behaviors don't depend on the status mapping:

  • A keyed miss answers 404 "No record found". When readOne, updateOne or deleteOne answers { record: null }, the REST API answers 404. The body is {"message":"Query failed","source":{"message":"No record found"}}. It's not an error your connector throws, so no message of yours is involved.
  • An undeclared comparison reaches your connector. Engine doesn't check the filters it sends against the called member's declaration, so what your connector throws for one decides what the caller sees. See Tell QueryRejected from a Plain Error.
  • A deactivated data source refuses every request that reaches its connector until the extension is reloaded, with the status of the error that deactivated it. A result the cache already holds is still served. This includes the request whose setup threw InvalidConfiguration or AccessDenied: the caller sees 422, with your message nested inside. See Know What Deactivates a Data Source.
  • A build that changes the query capabilities deactivates existing data sources when automatic reloading loads it, with 503 until the instance restarts or the workspace is rebuilt. See When the Capabilities Change.
  • Testing a configuration starts a fresh connector. A connection test or schema preview doesn't reuse a data source's connector, so a failed attempt doesn't block the next one, and neither needs a reload.
  • testConnection answering { ok: false } is reported as a failed connection.
  • A connector without testConnection still creates a data source, and a connection test answers 422 code 7002, "The connector does not offer a connection test". Engine runs testConnection, when you implement it, before it creates a data source or previews its schema.
  • Keep messages readable. Engine's own message is generic, and yours carries the specifics. Name the limit and what to change (e.g., "The Art Institute search API returns only the first 1000 results. Narrow the filter or read an earlier page.").

Recognize Errors Engine Raises

Engine fails a request with one of these messages when your connector breaks the protocol. For an answer that doesn't fit the protocol or the declared schema (the first four rows), the caller sees 500 "Invalid extension response", with the message nested inside. A value that doesn't fit an int32 field answers:

response.json
{
  "message": "Query failed",
  "source": {
    "message": "Invalid extension response",
    "source": {
      "message": "The extension returned a record that does not match its declared schema",
      "source": {
        "message": "Expected a value matching `Int32`, got a string"
      }
    }
  }
}

The two messages about missing handlers carry code -32601:

MessageCause
Missing selected field <field>. A record must carry every selected field, using an explicit null where it has no valueAn item in records, or a keyed answer's record, lacks a field listed in select
Expected a value matching <Type>, got a <kind>A value doesn't fit its field's declared type
Expected an array for a list-typed fieldA list field's value isn't an array or null
<handler> answered with something this protocol does not define: <detail>An answer has a key or a shape the protocol doesn't define. A keyed operation that answers records reads "unknown field records, expected record"
<handler> returned no valueA handler returned undefined or null. A keyed miss answers { record: null }, not null
preflight must answer synchronouslypreflight returned a promise
setup did not return a handler objectsetup returned something other than an object. It fails at startup and stops the data source until the extension reloads, as a missing introspect or query handler does
the connector returned no <handlers> handlersetup returned handlers without introspect or query. The message lists each missing one (e.g., "the connector returned no introspect, query handler")

The answer checks run after your handler returns, so a write your connector already made stays applied when its answer fails them.

See Also

MonospaceThe governed API layer for every app, person, and agent.

Copyright © 2026 Monospace Inc.