Errors and error codes#

The public exception hierarchy raised by the factgraph SDK, the attributes carried on each error, and the exported error-code string constants.

Most public error classes and all code constants are re-exported from the top-level factgraph.sdk package (see reference/factgraph.md). The base class SDKError is not re-exported from factgraph.sdk; import it from factgraph.sdk.errors. RuleValidationError and DetachedRowError live outside the SDK package; DetachedRowError is re-exported from factgraph.sdk, while RuleValidationError is not.

The .code and .path convention#

Every SDK error derives from a common base, SDKError, whose constructor accepts an optional machine-readable code and an optional path:

class SDKError(Exception):
    def __init__(self, message: str, *, code: str | None = None, path: str | None = None) -> None:
        super().__init__(message)
        self.code = code
        self.path = path
  • code is a stable string identifier (for example "ENTITY_ALREADY_EXISTS", "SCHEMA_CONFLICT"). It is None when not set.
  • path is a dotted location string (for example "$.test"), or None.

Both attributes are present on every error instance because every error class inherits this base. Catch broadly on SDKError and branch on .code:

from factgraph.sdk.errors import SDKError

try:
    fg.schema.register(User)
except SDKError as exc:
    if exc.code == "SCHEMA_CONFLICT":
        ...

Exception hierarchy#

Bases are defined in factgraph._sdk_errors; rule, DSL, and evaluation errors are defined under factgraph.application.protocol and factgraph.sdk.dsl.

Exception
└── SDKError                       (factgraph.sdk.errors.SDKError)
    ├── SDKSchemaError
    ├── SDKDSLError
    │   ├── DSLToApplicationRuleError
    │   ├── RuleExprError
    │   │   └── ExplicitBoolError
    │   └── (SDKDSLError raised directly)
    └── SDKStoreError
        ├── SDKValueError
        ├── EntityNotFoundError
        ├── EntityAlreadyExistsError
        ├── SchemaConflictError
        ├── SchemaNotFoundError
        ├── SchemaNonAdditiveError
        ├── FrozenSnapshotError
        ├── CardinalityError
        └── EditorClosedError

ValueError
└── RuleValidationError            (factgraph.application.protocol.rule.RuleValidationError)

RuntimeError
└── DetachedRowError               (factgraph.application.protocol.evaluate_result.DetachedRowError)

RuleValidationError derives from the Python built-in ValueError, and DetachedRowError derives from RuntimeError. Neither is part of the SDKError tree.

Store and schema errors#

These derive from SDKError and are raised by the read/write/schema surface. SDKStoreError and SDKSchemaError are themselves public and are also used directly as generic fallbacks.

SDKError#

class SDKError(Exception):
    def __init__(self, message: str, *, code: str | None = None, path: str | None = None) -> None

Root of the SDK exception hierarchy. Carries .code and .path. Importable from factgraph.sdk.errors.

SDKSchemaError#

class SDKSchemaError(SDKError):
    pass

Raised when schema input is structurally invalid before any store mutation, for example an empty class list, a class that is not an Entity subclass, or get()/edit() called with non-identity or missing identity fields.

raise SDKSchemaError("classes must be non-empty list[Entity|Relationship]")

SDKStoreError#

class SDKStoreError(SDKError):
    pass

The general store-runtime error and the fallback for application-layer errors that have no more specific SDK type. Raised directly for invalid call arguments to read/write namespaces and for unmapped application error codes; in the latter case the application code is preserved on .code.

raise SDKStoreError(err.message, code=err.code)

SDKValueError#

class SDKValueError(SDKStoreError):
    pass

Raised when a field value fails validation. Carries the application code "FIELD_VALUE_VALIDATION_FAILED" and a dotted path to the offending value.

raise SDKValueError(err.message, code="FIELD_VALUE_VALIDATION_FAILED", path=path)

EntityNotFoundError#

class EntityNotFoundError(SDKStoreError):
    def __init__(
        self,
        message: str,
        entity_type: str | None = None,
        identity_kwargs: dict | None = None,
        code: str | None = None,
        path: str | None = None,
    ) -> None

Raised when an entity addressed by identity does not exist, for example when opening an editor on a missing entity (fg.entities.edit(...)), or when the application layer reports code "ENTITY_NOT_FOUND".

AttributeTypeDescription
entity_typestr | NoneName of the entity class.
identity_kwargsdictCopy of the identity bundle used to address the entity. Defaults to {}.
codestr | None"ENTITY_NOT_FOUND" when remapped from an application error.
with pytest.raises(EntityNotFoundError):
    fg.entities.edit(User, user_id="missing")

EntityAlreadyExistsError#

class EntityAlreadyExistsError(SDKStoreError):
    def __init__(
        self,
        message: str,
        entity_type: str | None = None,
        identity_kwargs: dict | None = None,
        e_ref: str | None = None,
        code: str | None = None,
        path: str | None = None,
    ) -> None

Raised by fg.entities.create(...) when an entity with the supplied identity bundle is already visible in the ledger, including when that entity was materialized lazily by a prior fg.fields.set. The application code is "ENTITY_ALREADY_EXISTS".

AttributeTypeDescription
entity_typestr | NoneName of the entity class.
identity_kwargsdictCopy of the supplied identity bundle. Defaults to {}.
e_refstr | NoneEncoded reference of the entity that already exists.
codestr | None"ENTITY_ALREADY_EXISTS".
fg.entities.create(User, user_id="alice", tenant_id="acme")
with pytest.raises(EntityAlreadyExistsError) as exc_info:
    fg.entities.create(User, user_id="alice", tenant_id="acme")
err = exc_info.value
assert err.code == "ENTITY_ALREADY_EXISTS"
assert err.entity_type == "User"
assert err.identity_kwargs == {"user_id": "alice", "tenant_id": "acme"}

CardinalityError#

class CardinalityError(SDKStoreError):
    def __init__(
        self,
        message: str,
        field_name: str | None = None,
        actual_cardinality: str | None = None,
        operation: str | None = None,
        code: str | None = None,
        path: str | None = None,
    ) -> None

Raised when a write operation does not match a field’s declared cardinality: calling .set (or fg.fields.set) on a multi field, or .add (or fg.fields.add) on a single field. The application code is "FIELD_CARDINALITY_MISMATCH".

AttributeTypeDescription
field_namestr | NoneName of the field.
actual_cardinalitystr | NoneThe field’s declared cardinality (for example "single", "multi").
operationstr | NoneThe attempted operation ("set" or "add").
codestr | None"FIELD_CARDINALITY_MISMATCH".
with self.assertRaises(CardinalityError) as ctx:
    sdk.fields.set(User.tag, alice, "admin")
self.assertEqual(ctx.exception.code, "FIELD_CARDINALITY_MISMATCH")
self.assertEqual(ctx.exception.operation, "set")
self.assertEqual(ctx.exception.actual_cardinality, "multi")

FrozenSnapshotError#

class FrozenSnapshotError(SDKStoreError):
    pass

Raised on any attempt to mutate a read-only snapshot view, for example writing through an EntitySnapshot or an AssertionView.

raise FrozenSnapshotError("EntitySnapshot is read-only")

SchemaConflictError#

class SchemaConflictError(SDKStoreError):
    pass

Raised by fg.schema.register(...) (and by fg.schema.add(...) when it resolves to a register) when the entity type is already registered, or when a register would not be additive. The code is "SCHEMA_CONFLICT".

with pytest.raises(SchemaConflictError) as exc_info:
    fg.schema.register(User)  # already registered
assert exc_info.value.code == "SCHEMA_CONFLICT"

SchemaNotFoundError#

class SchemaNotFoundError(SDKStoreError):
    pass

Raised by fg.schema.extend(...) when the target entity type has not been registered. The code is "SCHEMA_NOT_FOUND".

with pytest.raises(SchemaNotFoundError) as exc_info:
    fg.schema.extend(Unregistered)
assert exc_info.value.code == "SCHEMA_NOT_FOUND"

SchemaNonAdditiveError#

class SchemaNonAdditiveError(SDKStoreError):
    pass

Raised by fg.schema.extend(...) (and by fg.schema.add(...) when it resolves to an extend) when the requested change to an existing entity type is not purely additive. The code is "SCHEMA_NON_ADDITIVE".

with pytest.raises(SchemaNonAdditiveError) as exc_info:
    fg.schema.extend(UserWithChangedIdentity)
assert exc_info.value.code == "SCHEMA_NON_ADDITIVE"

EditorClosedError#

class EditorClosedError(SDKStoreError):
    pass

Raised when an operation is attempted on an entity editor that has already been closed.

raise EditorClosedError("editor is closed")

Rule and DSL errors#

Authoring-time errors raised while building rules, lowering the SDK DSL, and validating rule expressions. RuleExprError, ExplicitBoolError, DSLToApplicationRuleError, and SDKDSLError are re-exported from factgraph.sdk.

SDKDSLError#

class SDKDSLError(SDKError):
    pass

Base for DSL and rule-authoring errors. A subclass of SDKError, so it carries .code and .path. Raised directly for some DSL lowering failures, carrying a code and path describing the failure location.

self.assertTrue(issubclass(SDKDSLError, SDKError))
raise SDKDSLError("dsl problem", code="X", path="$.test")

DSLToApplicationRuleError#

class DSLToApplicationRuleError(SDKDSLError):
    pass

Raised by build_application_rule(...) when SDK DSL input cannot be lowered into an application Rule, for example when a when body is empty, contains an OR branch or Case (the application Rule bridge accepts AND-only when bodies), or when ports are malformed or reference a LogicVar that does not appear in when.

with self.assertRaises(DSLToApplicationRuleError):
    build_application_rule(...)  # OR branch in when body

RuleExprError#

class RuleExprError(SDKDSLError):
    pass

Raised when RuleExpr authoring input violates the expression contract. Sources include: RuleExpr.all/RuleExpr.any called with no operands; RuleExpr.join called without constraints or with constraints that do not connect distinct occurrences; join_by_ports called without port names; inspecting or evaluating an expression whose head is not closed (unbound ports); requesting a port that the expression does not declare; and other shape violations during lowering and inspection.

with self.assertRaisesRegex(RuleExprError, "require at least one operand"):
    RuleExpr.all()

ExplicitBoolError#

class ExplicitBoolError(RuleExprError):
    pass

Raised when a Rule or RuleExpr value is used in a Python boolean context (for example with and, or, not, or an if). Rule.__bool__ and RuleExpr.__bool__ raise this instead of returning a truth value, to force the use of the & and | combinators.

raise ExplicitBoolError(
    "RuleExpr values do not support Python truthiness; use & or | instead of and/or"
)

RuleValidationError#

class RuleValidationError(ValueError):
    pass

Raised when an application protocol Rule violates its shape invariants, for example an empty when tuple, empty or malformed ports, a port Var that does not appear in where, an invalid repr template, or a projection request with duplicate or non-string port names. Derives from the built-in ValueError, not from SDKError. Defined in factgraph.application.protocol.rule.

raise RuleValidationError("when must be non-empty tuple[Atom, ...]")

Evaluation errors#

DetachedRowError#

class DetachedRowError(RuntimeError):
    pass

Raised when a live-only operation is requested on an EvaluateRow that is detached from (or stale with respect to) its EvaluateResult. Derives from the built-in RuntimeError, not from SDKError. Re-exported from factgraph.sdk. Defined in factgraph.application.protocol.evaluate_result. See reference/evaluation.md for the evaluation result types.

with self.assertRaisesRegex(DetachedRowError, "detached"):
    row.some_live_operation()

Error-code constants#

String constants exported from factgraph.sdk for use when matching on .code. They are defined in factgraph.sdk.error_codes.

ConstantValue
INVALID_ROW_FORMAT"INVALID_ROW_FORMAT"
QUERY_MISSING_REF"QUERY_MISSING_REF"
QUERY_TYPE_MISMATCH"QUERY_TYPE_MISMATCH"
QUERY_ALIAS_CONFLICT"QUERY_ALIAS_CONFLICT"
QUERY_UNBOUND_VAR"QUERY_UNBOUND_VAR"
QUERY_INVALID_ROW_FORMAT"QUERY_INVALID_ROW_FORMAT"
QUERY_NOT_IMPLEMENTED"QUERY_NOT_IMPLEMENTED"

These constants are the exported code values. Other code strings appear on errors as literals set at the raise site, including "ENTITY_ALREADY_EXISTS", "ENTITY_NOT_FOUND", "FIELD_CARDINALITY_MISMATCH", "FIELD_VALUE_VALIDATION_FAILED", "SCHEMA_CONFLICT", "SCHEMA_NOT_FOUND", and "SCHEMA_NON_ADDITIVE".

The QUERY_* constants name codes carried by the legacy query path, which is not covered by this documentation set.

comments powered by Disqus