Error Codes
SochDB uses a cross-language error taxonomy with machine-readable numeric codes.
The same code range means the same thing in every SDK, so you can switch on
error.code (Python / Node.js) or match on a sentinel (Go) and write
remediation logic that ports between languages.
The taxonomy is defined most completely in the Python SDK (sochdb 0.5.9), which
ships the full ErrorCode IntEnum and a deep exception hierarchy. The Node.js SDK
(@sochdb/sochdb 0.5.3) defines a subset of codes plus typed error classes, and the Go SDK
(github.com/sochdb/sochdb-go 0.4.5) exposes sentinel error values plus error structs with
Is() matchers. The numeric values that exist in more than one SDK are identical.
Code ranges at a glance
Every code is a 4- or 5-digit integer. The leading digits identify the category.
| Range | Category | Meaning | First-line remediation |
|---|---|---|---|
1xxx | Connection / transport | Could not reach or stay connected to the server or open the embedded engine | Check the address/path, retry with backoff, verify the server is running |
2xxx | Transaction | Transaction aborted, conflicted, expired, or not found | Retry the transaction; reduce write contention |
3xxx | Namespace / scope | Namespace missing, duplicate, invalid name, read-only, or access denied | Create the namespace first, or check tenant scope |
4xxx | Collection | Collection missing, duplicate, frozen, or misconfigured | Create the collection, or fix its config |
5xxx | Query | Query invalid, timed out, cancelled, or too large | Add filters, paginate, raise the timeout |
6xxx | Validation | Bad vector dimension, metadata, id, filter, or missing field | Fix the request payload before retrying |
7xxx | Resource | Not found, already exists, quota or resource exhausted | Back off, free resources, or pick another id |
8xxx | Authorization | Unauthorized, forbidden, token expired, scope violation | Check the capability token / credentials |
9xxx | Internal | Internal error, not implemented, storage error, FFI error | File a bug with the message and context |
10xxx | Lock / concurrency | Database locked, lock timeout, epoch mismatch, split-brain, stale lock | Ensure a single active writer; re-open the database |
ScopeViolationError is a validation class in Python but carries the authorization
code 8004 (SCOPE_VIOLATION). Switch on the numeric code when you need the exact
category; switch on the exception type when you want the semantic grouping.
Python SDK (0.5.9)
All exceptions are importable from the top-level sochdb package and derive from
SochDBError, which carries .code (an ErrorCode), .message, .remediation, and
.context. SochDBError.to_dict() gives you a JSON-serializable record.
from sochdb import SochDBError, ErrorCode, TransactionConflictError
try:
txn.commit()
except TransactionConflictError as e:
# Typed catch: retry on SSI conflict
print(e.code) # ErrorCode.TRANSACTION_CONFLICT
print(int(e.code)) # 2002
print(e.remediation) # "Retry the transaction. ..."
print(e.to_dict()) # {"code": 2002, "code_name": "TRANSACTION_CONFLICT", ...}
except SochDBError as e:
# Catch-all: branch on the numeric range
if 1000 <= e.code < 2000:
reconnect()
raise
ErrorCode IntEnum
These are the exact values defined in sochdb/errors.py. The enum is an IntEnum,
so ErrorCode.QUERY_TIMEOUT == 5002 and int(ErrorCode.QUERY_TIMEOUT) == 5002.
| Code | Name | Mapped exception |
|---|---|---|
1001 | CONNECTION_FAILED | ConnectionError |
1002 | CONNECTION_TIMEOUT | ConnectionError* |
1003 | CONNECTION_CLOSED | ConnectionError* |
1004 | PROTOCOL_ERROR | ProtocolError |
2001 | TRANSACTION_ABORTED | TransactionError |
2002 | TRANSACTION_CONFLICT | TransactionConflictError |
2003 | TRANSACTION_EXPIRED | TransactionError* |
2004 | TRANSACTION_READ_ONLY | TransactionError* |
2005 | TRANSACTION_NOT_FOUND | TransactionError* |
3001 | NAMESPACE_NOT_FOUND | NamespaceNotFoundError |
3002 | NAMESPACE_ALREADY_EXISTS | NamespaceExistsError |
3003 | NAMESPACE_INVALID_NAME | NamespaceError* |
3004 | NAMESPACE_ACCESS_DENIED | NamespaceAccessError |
3005 | NAMESPACE_READ_ONLY | NamespaceError* |
4001 | COLLECTION_NOT_FOUND | CollectionNotFoundError |
4002 | COLLECTION_ALREADY_EXISTS | CollectionExistsError |
4003 | COLLECTION_INVALID_CONFIG | CollectionConfigError |
4004 | COLLECTION_FROZEN | CollectionError* |
5001 | QUERY_INVALID | QueryError* |
5002 | QUERY_TIMEOUT | QueryTimeoutError |
5003 | QUERY_CANCELLED | QueryError* |
5004 | QUERY_TOO_LARGE | QueryError* |
6001 | INVALID_VECTOR_DIMENSION | DimensionMismatchError |
6002 | INVALID_METADATA | InvalidMetadataError |
6003 | INVALID_ID | ValidationError* |
6004 | INVALID_FILTER | ValidationError* |
6005 | MISSING_REQUIRED_FIELD | ValidationError* |
7001 | NOT_FOUND | SochDBError* |
7002 | ALREADY_EXISTS | SochDBError* |
7003 | QUOTA_EXCEEDED | SochDBError* |
7004 | RESOURCE_EXHAUSTED | SochDBError* |
8001 | UNAUTHORIZED | SochDBError* |
8002 | FORBIDDEN | SochDBError* |
8003 | TOKEN_EXPIRED | SochDBError* |
8004 | SCOPE_VIOLATION | ScopeViolationError |
9001 | INTERNAL_ERROR | SochDBError / DatabaseError |
9002 | NOT_IMPLEMENTED | EmbeddingError |
9003 | STORAGE_ERROR | SochDBError* |
9004 | FFI_ERROR | SochDBError* |
10001 | DATABASE_LOCKED | DatabaseLockedError |
10002 | LOCK_TIMEOUT | LockTimeoutError |
10003 | EPOCH_MISMATCH | EpochMismatchError |
10004 | SPLIT_BRAIN | SplitBrainError |
10005 | STALE_LOCK | LockError* |
* = generic mappingCodes marked * are defined in the enum, but from_rust_error() does not have a
dedicated subclass for them — they surface as the nearest base class
(for example ValidationError or SochDBError) carrying the specific code.
Only the rows without * have an entry in the internal _ERROR_MAP. Always read
e.code for the exact category rather than relying solely on the exception type.
Exception hierarchy
SochDBError
├── ConnectionError (1001)
├── TransactionError (2001)
│ └── TransactionConflictError (2002)
├── ProtocolError (1004)
├── DatabaseError (9001)
├── NamespaceError
│ ├── NamespaceNotFoundError (3001)
│ ├── NamespaceExistsError (3002)
│ └── NamespaceAccessError (3004)
├── CollectionError
│ ├── CollectionNotFoundError (4001)
│ ├── CollectionExistsError (4002)
│ └── CollectionConfigError (4003)
├── ValidationError
│ ├── DimensionMismatchError (6001) -> .expected, .actual
│ ├── InvalidMetadataError (6002)
│ └── ScopeViolationError (8004)
├── QueryError
│ └── QueryTimeoutError (5002) -> .context["timeout_seconds"]
├── EmbeddingError (9002)
└── LockError (10001)
├── DatabaseLockedError (10001) -> .context["path"], ["holder_pid"]
├── LockTimeoutError (10002) -> .context["path"], ["timeout_secs"]
├── EpochMismatchError (10003) -> .context["expected"], ["actual"]
└── SplitBrainError (10004)
Several classes attach structured context you can read directly:
from sochdb import DimensionMismatchError, DatabaseLockedError
try:
collection.insert(vector, metadata={})
except DimensionMismatchError as e:
print(e.context["expected"], e.context["actual"]) # e.g. 384 128
print(e.code) # ErrorCode.INVALID_VECTOR_DIMENSION
try:
db = sochdb.Database.open("./data")
except DatabaseLockedError as e:
print(e.context["path"], e.context.get("holder_pid"))
NamespaceError and CollectionError also expose convenience properties
(e.namespace, e.collection) that read from .context.
Transaction conflict (the one you will retry)
Transaction.commit() raises TransactionConflictError (code 2002) on a
Serializable Snapshot Isolation conflict, which corresponds to FFI code -2.
This is the canonical retry case:
from sochdb import TransactionConflictError
for attempt in range(5):
try:
with db.transaction() as txn:
txn.put(b"counter", str(value).encode())
break
except TransactionConflictError:
if attempt == 4:
raise
# back off and retry
Mapping engine codes
from_rust_error(code, message, context=None) -> SochDBError is the helper the FFI
bindings use to turn an engine error code into the correct typed exception. You can
call it yourself if you receive a raw (code, message) pair:
from sochdb import from_rust_error
err = from_rust_error(10001, "locked", {"path": "./data", "holder_pid": 4321})
isinstance(err, sochdb.DatabaseLockedError) # True
Node.js SDK (0.5.3)
The Node.js SDK defines a subset of the taxonomy. Every error extends
SochDBError, which carries .code (an ErrorCode enum value), .message, and an
optional .remediation string. Catch by instanceof or branch on .code.
import {
SochDBError,
ErrorCode,
TransactionError,
DatabaseLockedError,
} from '@sochdb/sochdb';
try {
await txn.commit();
} catch (err) {
if (err instanceof TransactionError) {
// SSI conflict surfaces here (FFI error_code === -2)
console.error(err.code); // ErrorCode.TRANSACTION_ABORTED (2001)
retry();
} else if (err instanceof DatabaseLockedError) {
console.error(err.path, err.holderPid);
} else if (err instanceof SochDBError) {
console.error(err.code, err.remediation);
}
}
ErrorCode enum (Node.js)
These are the codes actually declared in src/errors.ts:
| Code | Name | Class |
|---|---|---|
1001 | CONNECTION_FAILED | ConnectionError |
1002 | CONNECTION_TIMEOUT | (code only) |
1003 | CONNECTION_CLOSED | (code only) |
1004 | PROTOCOL_ERROR | ProtocolError |
2001 | TRANSACTION_ABORTED | TransactionError |
2002 | TRANSACTION_CONFLICT | (code only) |
9001 | INTERNAL_ERROR | SochDBError (default) |
9003 | STORAGE_ERROR | DatabaseError |
10001 | DATABASE_LOCKED | DatabaseLockedError |
10002 | LOCK_TIMEOUT | LockTimeoutError |
10003 | EPOCH_MISMATCH | EpochMismatchError |
10004 | SPLIT_BRAIN | SplitBrainError |
10005 | STALE_LOCK | (code only) |
The Node.js enum does not include the 3xxx, 4xxx, 5xxx, 6xxx, 7xxx, or
8xxx ranges. Namespace and collection failures are surfaced through dedicated classes
(NamespaceNotFoundError, NamespaceExistsError, CollectionNotFoundError,
CollectionExistsError, all extending SochDBError) rather than numbered codes. Do not
assume a numeric code exists in Node.js just because Python defines it.
Class hierarchy (Node.js)
Error
└── SochDBError (code, remediation)
├── ConnectionError (1001)
├── TransactionError (2001)
├── ProtocolError (1004)
├── DatabaseError (9003)
├── LockError (10001)
│ ├── DatabaseLockedError (10001) -> .path, .holderPid
│ ├── LockTimeoutError (10002) -> .path, .timeoutSecs
│ ├── EpochMismatchError (10003) -> .expected, .actual
│ └── SplitBrainError (10004)
├── NamespaceNotFoundError (from namespace.ts)
├── NamespaceExistsError
├── CollectionNotFoundError
└── CollectionExistsError
StudioClient throws StudioAPIError(message, statusCode?) and the MCP layer throws
McpError(message, code, data?); both extend Error (with MCP_ERROR_CODES exported
for the MCP JSON-RPC code set) rather than SochDBError.
Go SDK (0.4.5)
The Go SDK follows Go idioms: sentinel errors for errors.Is() matching, plus
error structs for the cases that carry structured fields. The lock-family structs
implement Is() so they match their corresponding sentinel.
import (
"errors"
"github.com/sochdb/sochdb-go"
)
// Sentinel match (works through wrapping and through struct Is())
if errors.Is(err, sochdb.ErrDatabaseLocked) {
// back off and retry, or surface to the operator
}
// Typed match when you need the fields
var locked *sochdb.DatabaseLockedError
if errors.As(err, &locked) {
log.Printf("locked at %s by pid %d", locked.Path, locked.HolderPID)
}
Sentinel errors
Defined as package-level vars in errors.go:
| Sentinel | Message |
|---|---|
ErrClosed | connection closed |
ErrNotFound | key not found |
ErrInvalidResponse | invalid server response |
ErrDatabaseLocked | database locked by another process |
ErrLockTimeout | timed out waiting for database lock |
ErrEpochMismatch | epoch mismatch: stale writer detected |
ErrSplitBrain | split-brain: multiple active writers |
Error structs
| Struct | Fields | Is() sentinel |
|---|---|---|
ConnectionError | Address, Err (has Unwrap()) | — |
ProtocolError | Message | — |
ServerError | Message | — |
TransactionError | Message | — |
SochDBError | Op, Message | — |
LockError | Path, Message, Remediation | — |
DatabaseLockedError | Path, HolderPID | ErrDatabaseLocked |
LockTimeoutError | Path, TimeoutSecs | ErrLockTimeout |
EpochMismatchError | Expected, Actual (uint64) | ErrEpochMismatch |
SplitBrainError | Message | ErrSplitBrain |
NamespaceNotFoundError | Namespace | — |
NamespaceExistsError | Namespace | — |
CollectionNotFoundError | Collection | — |
CollectionExistsError | Collection | — |
FormatConversionError | FromFormat, ToFormat, Reason | — |
The Go SDK has no ErrorCode enum — there are no 1xxx/10xxx integers to switch
on. Match with errors.Is() (sentinels) or errors.As() (structs). The lock structs
embed remediation in LockError.Remediation / the error message text.
Transaction conflict (Go)
In the embedded engine (built with -tags sochdb_embedded), a Serializable Snapshot
Isolation conflict surfaces from Transaction.Commit() as a *TransactionError whose
message is SSI conflict: transaction aborted due to serialization failure (FFI code
-2). Commit() returns error only — there is no commit timestamp in the Go return:
err := db.WithTransaction(func(txn *embedded.Transaction) error {
return txn.Put([]byte("counter"), []byte("1"))
})
var txErr *sochdb.TransactionError
if errors.As(err, &txErr) {
// retry on serialization failure
}
Lock and concurrency errors (all SDKs)
The 10xxx lock family is the most operationally important set: it tells you another
process is contending for the same embedded database directory. It exists in all three
SDKs with matching semantics.
| Condition | Python | Node.js | Go | What it means |
|---|---|---|---|---|
| Locked by another process | DatabaseLockedError (10001) | DatabaseLockedError (10001) | ErrDatabaseLocked / DatabaseLockedError | Another process already holds the single-writer lock |
| Lock acquisition timed out | LockTimeoutError (10002) | LockTimeoutError (10002) | ErrLockTimeout / LockTimeoutError | Waited too long for the lock; possible deadlock |
| WAL epoch mismatch | EpochMismatchError (10003) | EpochMismatchError (10003) | ErrEpochMismatch / EpochMismatchError | A newer writer took over; your handle is stale |
| Split-brain | SplitBrainError (10004) | SplitBrainError (10004) | ErrSplitBrain / SplitBrainError | Multiple active writers detected; data integrity risk |
| Stale lock | code 10005 (STALE_LOCK) | code 10005 (STALE_LOCK) | — | A leftover lock from a crashed process |
If you need multiple readers alongside a writer in the same process, open with the
concurrent path instead of fighting the single-writer lock: Database.open_concurrent(path)
(Python), EmbeddedDatabase.openConcurrent(path) (Node.js), or
embedded.OpenConcurrent(path) (Go). See the language guides for details:
/docs/api-reference/python-api.
Cross-SDK comparison
| Capability | Python (0.5.9) | Node.js (0.5.3) | Go (0.4.5) |
|---|---|---|---|
Numeric ErrorCode enum | Full (1xxx–10xxx) | Subset (1xxx, 2xxx, 9xxx, 10xxx) | None |
| Base type | SochDBError(Exception) | class SochDBError extends Error | SochDBError struct + sentinels |
| Remediation field | .remediation + .context | .remediation | LockError.Remediation / message text |
| Conflict-detection class | TransactionConflictError | TransactionError | *TransactionError |
| Matching idiom | except SubclassError / check .code | instanceof / check .code | errors.Is / errors.As |
| Serialize to dict/JSON | e.to_dict() | read .code, .message, .remediation | format .Error() |