Error handling¶
KGLite exposes a typed Python exception hierarchy for engine, Cypher, schema,
transaction, and storage failures. Catch the narrowest class you can recover
from; catch kglite.KgError when every KGLite engine failure has the same
handling policy.
Exception hierarchy¶
Exception
└── kglite.KgError
├── kglite.CypherError
│ ├── kglite.CypherSyntaxError
│ ├── kglite.CypherTimeoutError
│ ├── kglite.CypherExecutionError
│ └── kglite.CypherTypeMismatchError
├── kglite.SchemaError
├── kglite.ValidationError
├── kglite.ExprError
├── kglite.ConstraintError
│ ├── kglite.ConstraintViolationError
│ └── kglite.ConstraintCreationError
├── kglite.TransactionConflictError
├── kglite.NodeNotFoundError
├── kglite.ConnectionNotFoundError
├── kglite.PropertyNotFoundError
├── kglite.FileError
├── kglite.FileFormatError
├── kglite.FileIoError
├── kglite.LoadMemoryLimitError
├── kglite.ArgumentError
├── kglite.MissingArgumentError
├── kglite.InternerCollisionError
└── kglite.InternalError
CypherSyntaxError always has .line and .col attributes (either may be
None). CypherExecutionError has them when the executor can identify the
source position. Timeout messages report the elapsed and configured limit.
Stable codes¶
Every instance carries .code, a stable classifier string — branch on that
rather than on message prose, which is free to improve between releases:
try:
graph.cypher(query)
except kglite.KgError as exc:
log.warning("kglite failed", extra={"kglite_code": exc.code})
.code is also readable on the concrete classes themselves
(kglite.ConstraintViolationError.code == "ConstraintViolation"), so a
dispatch table can be built up front. It is None on the three abstract bases
— KgError, CypherError, ConstraintError — which each span several codes.
The same strings appear as KGLITE_STATUS_* in the C ABI and drive the Bolt
Neo.* status mapping, so one code means the same thing in every binding.
Constraint violations¶
A write that breaks a declared UNIQUE / NOT NULL / NODE KEY / IS :: TYPE
constraint raises ConstraintViolationError — from every write path,
cypher() and the bulk loaders alike. Relationship constraints
(FOR ()-[r:T]-() REQUIRE r.p IS NOT NULL / IS :: TYPE) raise the same
exception, with a message written in relationship words. Declaring a constraint the stored data already violates is a
different problem with a different fix, so it raises the sibling
ConstraintCreationError; both subclass ConstraintError.
try:
graph.cypher("CREATE (u:User {email: $email})", params={"email": email})
except kglite.ConstraintViolationError:
raise Conflict("that email is already registered")
The message names the constraint, the property, and the offending value, so it
is worth logging — but the type and .code are the contract.
Transaction conflicts¶
Transaction.commit() raises TransactionConflictError when the graph moved
since begin(). Nothing was applied, so the fix is to re-run the work against
a fresh begin() — see Transactions and sessions for retry_on_conflict, which is
that loop.
try:
tx.commit()
except kglite.TransactionConflictError:
... # rebuild the transaction and try again
A write on a read handle¶
Four handles refuse writes, and all four refuse with the same class and the
same code — ArgumentError / InvalidArgument:
Handle |
Refuses |
|---|---|
|
use |
|
an immutable snapshot; mutate the source graph and re- |
A transaction from |
use |
A graph under |
|
One policy gets one class, so an application routes on the refusal without matching four things:
try:
session.cypher(statement)
except kglite.ArgumentError as exc:
assert exc.code == "InvalidArgument"
The refusal is deliberately not CypherExecutionError: the query did not
fail to execute, it was aimed at a handle that does not take it. That
distinction is visible on the wire too — CypherExecution maps to
Neo.DatabaseError.Statement.ExecutionFailed, which tells a Bolt driver the
server broke, while InvalidArgument maps to
Neo.ClientError.Statement.ArgumentError.
The same rule covers an unknown node type: properties(),
neighbors_schema(), sample(), describe(types=[...]), set_parent_type()
and set_temporal() all raise ArgumentError for a type the graph does not
have.
Catching errors¶
import kglite
try:
result = graph.cypher(query, params=params, timeout_ms=30_000)
except kglite.CypherSyntaxError as exc:
print(f"invalid query at {exc.line}:{exc.col}: {exc}")
except kglite.CypherTimeoutError:
print("rewrite, scope, or explicitly increase the deadline")
except kglite.CypherError as exc:
print(f"query failed: {exc}")
A timed-out Cypher query raises CypherTimeoutError; it does not return a
partial ResultView. Mutation execution restores a statement checkpoint on an
execution error, timeout, or work-budget refusal, including direct
KnowledgeGraph.cypher() calls. Previously successful statements remain intact.
Use an explicit Transaction when several statements must
commit or roll back together. This is an execution guarantee, not rollback of
later application-side result conversion or consumer errors.
For a broad engine boundary:
try:
graph = kglite.load("graph.kgl")
rows = graph.cypher(query)
except kglite.KgError as exc:
log.error("KGLite operation failed: %s", exc)
Built-in Python exceptions¶
KgError is not a wrapper around every Python failure. Python-facing
protocols retain conventional exceptions:
Situation |
Exception |
|---|---|
Missing result column or mapping key |
|
Invalid Python-side value or unsupported wrapper mode |
|
Wrong Python object or argument shape |
|
A query parameter outside the signed 64-bit integer range |
|
Wrapper-side path opening |
|
Borrow or object-lifecycle conflict |
|
User cancellation with Ctrl-C |
|
KeyboardInterrupt is deliberately outside KgError; an interrupt is a user
action, not a query fault. Catch it separately if the application needs
cleanup:
try:
graph.cypher(long_read, timeout_ms=0)
except KeyboardInterrupt:
print("cancelled")
Loading and recovery¶
Load failures are classifiable: a missing engine-managed path raises
FileError; malformed, truncated, or unsupported saved data raises
FileFormatError; other I/O failures raise FileIoError.
A fourth case is not a failure of the file at all. kglite.load(path, max_load_mb=N) — and the process-wide KGLITE_MAX_LOAD_MB — raise
LoadMemoryLimitError when the estimated peak is over the ceiling. Metadata-
known terms refuse before decompression. An older portable file containing
stored endpoint references needs a second conservative normalization-overlay
check after the affected values are decoded, but before the private graph is
changed or published. The graph is valid; this process cannot afford it.
Rebuilding would not help, which is exactly why it is its own class: raise the
ceiling, pass defer_index_rebuild=True (usually the largest metadata term), or
load it somewhere with more memory.
kglite.estimate_load_memory(path) reports the metadata-known terms as a dict.
It cannot see legacy reference values without decoding them, so an affected
file can pass that public estimate and still be refused on the additional
normalization term. The refusal message says which check fired.
budget_mb = 512
try:
graph = kglite.load("large.kgl", max_load_mb=budget_mb)
except kglite.LoadMemoryLimitError:
# The index rebuild is the term usually worth dropping.
graph = kglite.load("large.kgl", max_load_mb=budget_mb, defer_index_rebuild=True)
The ceiling compares an estimate read from the file’s metadata head, not a measurement, and it errs high on purpose — so a ceiling set close to a graph’s real cost can refuse a load that would have fitted. Set it where a failure is what you want (a serving process that must not be killed by a file it did not choose), not as a tight budget.
The write half classifies the same way: a save(), sync() or to_bytes()
that fails on I/O — a full disk, a read-only directory, a failing device —
raises FileIoError too, so except kglite.KgError covers both directions of
the file lifecycle. A save() refused before it touched the path (no
remembered path, or a write-ahead sidecar running ahead of the target) is a
ValueError instead: nothing was written, and the fix is a different argument
rather than a different disk.
try:
graph = kglite.load("cache.kgl")
except kglite.FileError:
graph = rebuild_from_source()
except kglite.FileFormatError:
graph = rebuild_from_source()
A CSV export is an interoperability view, not a byte-for-byte graph backup: labels, schema, indexes, embeddings, time series, and some structured values are not fully preserved. Keep the original source or a tested rebuild path; see Import and Export for the exact persistence and export contract.
Concurrency conflicts¶
Direct KnowledgeGraph objects follow Python ownership and borrow rules. For
shared readers and writers, use graph.session(). A transaction commits with
optimistic concurrency control; a stale snapshot raises a typed KgError
instead of silently overwriting a newer commit. See Transactions and sessions and
Concurrency.
Other bindings¶
Rust code matches on KgError or the stable classifier KgErrorCode. The
classifier also supplies canonical HTTP and Neo4j/Bolt status codes; each
binding still owns its response shape and lifecycle. The C ABI exposes the
corresponding KGLITE_STATUS_* codes declared in the generated header. See
C ABI for ownership and status details.
InternalError represents a broken KGLite invariant. It is not a recoverable
user-input condition; report it with the complete message and a minimal
reproducer.
See also¶
Python API reference — method-specific exceptions.
Transactions and sessions — rollback, optimistic commits, and shared sessions.
Cypher Queries — query deadlines, row caps, and diagnostics.
API reference — Rust error and execution-option boundary.
C ABI — non-Rust binding status codes.