Error Handling
Garak provides structured error responses that help you handle failures gracefully and debug issues effectively.
Error Response Structure
All errors from Garak follow this format:
{
"field": "fieldName",
"code": "ERROR_CODE",
"message": "Human readable error message",
"kind": "KIND_CONSTANT"
}
Properties
- field: The field that caused the error (empty string if not applicable)
- code: A unique error code for programmatic handling
- message: Human-readable description of the error
- kind: Category of error for broad classification
Error Kinds
KIND_OPERATION_INCOMPATIBLE_WITH_FIELD
The operation cannot be performed on the field due to type mismatch.
Common Scenarios:
- Trying to increment a non-numeric field
- Using
addToSetorpullon a non-array field
Example:
// Document: { name: "Alice", age: 25 }
// This will fail:
await tx.inc(store, nodeId, "name", 1);
// Error: field "name" is not a number
Error Codes:
ETARGETFIELDNOTINT- Field is not a numberETARGETFIELDNOTARR- Field is not an array
Handling:
try {
await tx.inc(store, nodeId, "field", 1);
} catch (error) {
if (error.kind === "KIND_OPERATION_INCOMPATIBLE_WITH_FIELD") {
if (error.code === "ETARGETFIELDNOTINT") {
console.error("Cannot increment non-numeric field");
} else if (error.code === "ETARGETFIELDNOTARR") {
console.error("Cannot add to non-array field");
}
}
}
KIND_NODE_NOT_FOUND
The requested node does not exist.
Common Scenarios:
- Querying non-existent node_id
- Node was deleted
- Wrong shard specified
Example:
const node = await tx.get(store, 999999);
// Error: Node 999999 not found in silo/shard
Error Code:
ENOTFOUND- Node not found
HTTP Status: 404
Handling:
try {
const node = await tx.get(store, nodeId);
} catch (error) {
if (error.kind === "KIND_NODE_NOT_FOUND") {
console.log("Node does not exist");
// Handle missing node (create new, use default, etc.)
}
}
KIND_SCHEMA_VIOLATION
The operation violates index constraints.
Common Scenarios:
- Inserting duplicate value in secondary key index
- Updating field to duplicate secondary key value
Example:
const emailIndex = new Garak.SecondaryKeyIndex(usersStore, "email");
// First insert succeeds
await tx.insert(usersStore, { email: "alice@example.com" });
// Second insert fails
await tx.insert(usersStore, { email: "alice@example.com" });
// Error: Duplicate value in secondary key index
Error Codes:
EDUPLICATE- Value violates uniqueness constraintEVIOLATION- General schema violation
Handling:
try {
await tx.insert(store, data);
} catch (error) {
if (error.kind === "KIND_SCHEMA_VIOLATION") {
if (error.code === "EDUPLICATE") {
console.error(`Duplicate value for field: ${error.field}`);
// Handle duplicate (update existing, show error to user, etc.)
}
}
}
KIND_MALFORMED_INDEX
The index is corrupted or misconfigured.
Common Scenarios:
- Index files corrupted on disk
- Index definition conflicts
- Concurrent index modifications
Error Codes:
EIDXCORRUPT- Index file corruptedEIDXCONFLICT- Index definition conflict
Handling:
try {
const result = await index.lookup(tx, key);
} catch (error) {
if (error.kind === "KIND_MALFORMED_INDEX") {
console.error("Index corrupted, needs rebuild");
// Trigger reindex or alert operations team
}
}
Malformed index errors usually require manual intervention. Consider triggering a reindex operation.
KIND_OVERSIZE_NODE
The node data exceeds the 25KB limit.
Common Scenarios:
- Inserting document > 25KB
- Large embedded objects
- Long text fields
Example:
const hugeData = {
content: "x".repeat(30_000) // 30KB string
};
await tx.insert(store, hugeData);
// Error: Node data too large (30000 bytes), Garak max size per node is 25kb
Error Code:
ERROR_NODE_TOO_LARGE- Node exceeds size limit
Handling:
try {
await tx.insert(store, data);
} catch (error) {
if (error.kind === "KIND_OVERSIZE_NODE") {
console.error("Document too large, consider splitting it");
// Split document, store large fields separately, etc.
}
}
Client Error Handling
TypeScript/Node.js
The TypeScript client provides a DatabaseError class:
import { Garak } from '@your-org/garak-client';
class DatabaseError extends Error {
field: string;
code: string;
db_message: string;
httpStatus: number;
kind: string;
}
// Usage
try {
await tx.insert(store, data);
} catch (error) {
if (error instanceof DatabaseError) {
console.error({
field: error.field,
code: error.code,
message: error.db_message,
httpStatus: error.httpStatus,
kind: error.kind
});
}
}
Rust
The Rust client uses a DatabaseError type:
use garak::{DatabaseError, DatabaseErrorKind};
match client.insert(&store, &data).await {
Ok(result) => println!("Inserted: {}", result.node_id),
Err(e) => match e.kind {
DatabaseErrorKind::KIND_SCHEMA_VIOLATION => {
eprintln!("Schema violation: {}", e.message);
}
DatabaseErrorKind::KIND_OVERSIZE_NODE => {
eprintln!("Node too large: {}", e.message);
}
_ => eprintln!("Error: {}", e.message)
}
}
HTTP Status Codes
Garak uses standard HTTP status codes:
| Status | Meaning | Common Causes |
|---|---|---|
| 200 | Success | Operation completed successfully |
| 202 | Accepted | Async job started (cold sync) |
| 400 | Bad Request | Invalid input, type mismatch, size limit |
| 401 | Unauthorized | Missing Authorization header |
| 403 | Forbidden | Invalid token or insufficient permissions |
| 404 | Not Found | Node doesn't exist, job not found |
| 500 | Internal Server Error | Unexpected server error |
Common Error Patterns
Handling Missing Nodes
async function getOrCreate(
tx: Transaction,
store: Store<User>,
nodeId: number
): Promise<User> {
try {
return await tx.get(store, nodeId);
} catch (error) {
if (error.kind === "KIND_NODE_NOT_FOUND") {
// Create default user
const { node_id } = await tx.insert(store, {
id: nodeId,
name: "Unknown",
createdAt: Date.now()
});
return await tx.get(store, node_id);
}
throw error;
}
}
Handling Duplicate Keys
async function upsertByEmail(
tx: Transaction,
store: Store<User>,
index: SecondaryKeyIndex<User>,
email: string,
data: Partial<User>
): Promise<number> {
// Try to find existing
const { node_id } = await index.lookup(tx, email);
if (node_id !== null) {
// Update existing
for (const [key, value] of Object.entries(data)) {
await tx.set(store, node_id, key, value);
}
return node_id;
}
// Insert new
try {
const result = await tx.insert(store, { email, ...data });
return result.node_id;
} catch (error) {
if (error.kind === "KIND_SCHEMA_VIOLATION" && error.code === "EDUPLICATE") {
// Race condition: another process inserted between lookup and insert
// Retry the operation
return await upsertByEmail(tx, store, index, email, data);
}
throw error;
}
}
Debugging Errors
Enable Verbose Logging
Set environment variable for detailed logs:
export RUST_LOG=debug
Inspect Error Details
Always log the full error object for debugging:
catch (error) {
console.error("Full error details:", {
name: error.name,
message: error.message,
field: error.field,
code: error.code,
kind: error.kind,
httpStatus: error.httpStatus,
stack: error.stack
});
}
Check Server Logs
Garak logs all operations with timing:
-> Request URL: /insert
Inserting data into shard: my-shard, store: users
Generating node_id: currently at 12345
Inserted node_id: 12346 (took 5 ms)
<- Responded to /insert in 5 ms
Best Practices
- Always handle KIND_NODE_NOT_FOUND: Nodes can be deleted concurrently
- Implement retry logic for EDUPLICATE: Handle race conditions
- Validate data size before insert: Avoid KIND_OVERSIZE_NODE at runtime
- Type-check before operations: Prevent KIND_OPERATION_INCOMPATIBLE_WITH_FIELD
- Log error context: Include node_id, operation, and state for debugging
- Monitor error rates: Track error kinds and codes for system health
- Fail fast: Don't silently swallow errors; propagate or handle explicitly
Error Recovery
Automatic Recovery
Some errors are automatically recoverable:
- Network timeouts: Retry with exponential backoff
- Temporary locks: Retry after brief delay
- Race conditions: Retry operation with updated state
Manual Recovery
Some errors require manual intervention:
- KIND_MALFORMED_INDEX: Trigger reindex operation
- Corrupted data files: Restore from backup
- Disk full: Free space or expand storage
- Master lock stuck: Clear lock file manually
Rollback Strategy
Since Garak doesn't have automatic transaction rollback:
async function multiStepOperation(tx: Transaction) {
const createdNodes: number[] = [];
try {
// Step 1
const { node_id: id1 } = await tx.insert(store1, data1);
createdNodes.push(id1);
// Step 2
const { node_id: id2 } = await tx.insert(store2, data2);
createdNodes.push(id2);
// Step 3 fails
await tx.insert(store3, invalidData);
} catch (error) {
// Manual rollback
for (const nodeId of createdNodes) {
await tx.delete(store1, nodeId).catch(() => {});
}
throw error;
}
}