Error handling

The typed exception hierarchy and the stable error codes behind it.

Every failure raised by drasi-lib is a DrasiError or a subclass of one, and carries a stable code you can branch on.

from drasi import DrasiError

try:
    await drasi.add_query("open", "MATCH (o:Order", ["orders"])
except DrasiError as err:
    print(err.code, err)

The hierarchy

DrasiError
├── ConfigError
├── UnknownKindError
├── SourceError
├── StreamLaggedError
└── PluginError
    ├── PluginNotFoundError
    ├── PluginCompatibilityError
    └── PluginSignatureError

Catching DrasiError catches everything from the library and nothing from anywhere else, so it is safe to wrap a whole block. Catching a subclass narrows to one kind of problem — PluginSignatureError on its own, say, if an unverified plugin should be fatal but a missing one should not.

Branch on the code, not the message

Messages are written for people and may be reworded. The code is part of the API:

from drasi import DrasiError

try:
    await drasi.push_change("orders", change)
except DrasiError as err:
    if err.code == "CHANGE_OP_REQUIRED":
        raise ValueError(f"malformed change from {origin}") from err
    raise

The full set is available at runtime as drasi.ERROR_CODES.

The codes

CodeRaised when
UNKNOWN_SOURCE_KINDNo source plugin is registered for that kind
UNKNOWN_REACTION_KINDNo reaction plugin is registered for that kind
UNKNOWN_BOOTSTRAP_KINDNo bootstrap plugin is registered for that kind
UNKNOWN_SECRET_STORE_KINDNo secret store plugin is registered for that kind
BOOTSTRAP_KIND_REQUIREDA bootstrap block was given without a kind
NO_PY_SOURCEThe id does not name a Python-defined source
CHANGE_NOT_OBJECTThe change was not a mapping
CHANGE_OP_REQUIREDThe change had no op
CHANGE_ID_REQUIREDThe change had no id
RELATION_REQUIRES_BOTH_ENDSOnly one of start_id / end_id was supplied
UNKNOWN_CHANGE_OPThe op was not insert, update or delete
STATE_STORE_PATH_REQUIREDA durable state store was configured without a path
UNKNOWN_STATE_STORE_KINDThe state store kind is not one this build supports
INDEX_STORE_PATH_REQUIREDAn index store was configured without a path
UNKNOWN_INDEX_STORE_KINDThe index store kind is not compiled into this wheel
IDENTITY_KIND_REQUIREDAn identity block was given without a kind
UNKNOWN_IDENTITY_KINDNo identity provider matches that kind
IDENTITY_CONFIG_INVALIDThe identity block is missing fields its kind needs
DURABLE_REQUIRES_STATE_STOREA durable reaction was added without a state store
UNKNOWN_QUERY_LANGUAGEThe language was not cypher or gql
CONFIG_INVALIDThe configuration was malformed
PLUGIN_SIGNATURE_INVALIDSignature verification failed
PLUGIN_INCOMPATIBLEThe plugin was built against a different Drasi version
PLUGIN_NOT_FOUNDNo such plugin in the registry, or no build for this platform
STREAM_LAGGEDA stream dropped items because they were not consumed fast enough
ENGINE_CLOSEDThe engine was closed and then asked to change
ENGINE_FAILUREThe engine itself reported a failure

Errors raised before the await

Arguments are validated on the call, not when the coroutine is awaited:

pending = drasi.push_change("orders", {"id": "o1"})  # raises here

This matters because a coroutine that raises only when awaited is easy to create and forget, and the failure then surfaces somewhere unrelated — or not at all if it is never awaited.

Using a closed engine

Once close() has been called, anything that would change the engine raises ENGINE_CLOSED:

await drasi.close()
await drasi.add_query(...)  # ENGINE_CLOSED

Reads are still allowed, because inspecting a closed engine is harmless and often useful. The alternative — accepting a component that can never run — hid the mistake until much later.

Next