Error handling

July 13, 2026 · View on GitHub

The Kotlin/Native client surfaces failures as unchecked exceptions. Every error is a subclass of MongrelDBException (itself a RuntimeException), so you can catch the category you care about and let the rest propagate. This is the complete reference: the exception hierarchy, the HTTP-status mapping, the daemon's error envelope, and recovery patterns for each category.


The exception model

The client uses a sealed hierarchy rooted at MongrelDBException:

RuntimeException
  └── MongrelDBException
        ├── AuthException          // HTTP 401 or 403
        ├── NotFoundException      // HTTP 404
        ├── ConflictException      // HTTP 409 (unique/fk/check violation)
        └── QueryException         // HTTP 400, 5xx, transport, malformed JSON

All four are unchecked, so you never need a throws clause. Catch the most specific type first; MongrelDBException is the catch-all. Native-mode failures (NativeDB) surface as QueryException, carrying the FFI error string from mongreldb_kit_last_error.

Exception reference

ExceptionHTTP statusMeaningTypical cause
AuthException401, 403authentication/authorization failureMissing/bad credentials against an auth-enabled daemon
NotFoundException404resource not foundMissing table, missing schema, dropped resource
ConflictException409constraint violation at commitUnique, foreign-key, check, or trigger violation
QueryException400, 5xxquery/server/transport failureMalformed request, server-side failure, network error, malformed JSON
MongrelDBException(base)any client errorCatch-all parent

ConflictException exposes two extra fields decoded from the daemon's error envelope:

  • code: String? - the structured server code (e.g. "UNIQUE_VIOLATION").
  • opIndex: Int? - the index of the offending operation in the batch.

The daemon's error envelope

When the daemon rejects a request, it returns a JSON envelope. The client decodes its message, code, and op_index into the exception:

{
  "status": "aborted",
  "error": {
    "code": "UNIQUE_VIOLATION",
    "message": "duplicate key in column 1",
    "op_index": 0
  }
}

Structured codes you will commonly see:

codeMeaning
UNIQUE_VIOLATIONA unique/PK constraint rejected the commit
FK_VIOLATIONA foreign-key reference was missing
CHECK_VIOLATIONA check constraint or trigger rejected the commit
NOT_FOUNDA named resource (table, schema) does not exist

HTTP status -> exception mapping

HTTP statusExceptionNotes
401, 403AuthExceptionBad/missing credentials
404NotFoundExceptionResource not found
409ConflictExceptionConstraint violation at commit
400QueryExceptionMalformed request / bad query
5xxQueryExceptionDaemon-side failure
other non-2xxQueryExceptionCatch-all
2xx(no exception)Success

Transport-level failures (libcurl connection refused, timeout, DNS failure) surface as QueryException wrapping the underlying cause.

Discriminating errors

Catch the specific type; fall back to the parent:

try {
    val body = db.schemaFor("missing_table")
} catch (e: NotFoundException) {
    System.err.println("table does not exist: ${e.message}")
} catch (e: ConflictException) {
    System.err.println("unexpected conflict on a read (code=${e.code}): ${e.message}")
} catch (e: AuthException) {
    System.err.println("bad credentials: ${e.message}")
} catch (e: MongrelDBException) {
    System.err.println("error: ${e.message}")
}

Recovery patterns

Auth failure - do not retry blindly

A retry will not fix bad credentials. Surface the error to the caller or operator.

try {
    db.commit(ops)
} catch (e: AuthException) {
    // Refresh credentials from your secret store, or fail fast.
    throw e
}

Not found - fall back, do not crash

For lookups by primary key, a 404 may be a normal "absent" result (when the table itself is missing). Treat it accordingly.

try {
    db.schemaFor(table)
} catch (e: NotFoundException) {
    // table missing - treat as empty
    return emptyList()
}

Note: a PrimaryKeyInt/PrimaryKeyString query against an existing table returns zero rows, not a 404; NotFoundException here means the table itself is missing.

Constraint conflict - the engine already rolled back

try {
    db.commit(ops)
} catch (e: ConflictException) {
    System.err.println("constraint violated (code=${e.code}, opIndex=${e.opIndex}): ${e.message}")
    // The engine already discarded the whole batch. Nothing to undo.
}

Transient failure - retry with an idempotency key

QueryException (for network/5xx errors) covers transport and transient server failures. With an idempotency key, retrying a transaction is safe (see transactions.md).

var lastError: MongrelDBException? = null
for (attempt in 0 until 3) {
    try {
        db.commit(ops, idempotencyKey = "stable-key")
        lastError = null
        break
    } catch (e: AuthException) {
        // not transient - stop
        throw e
    } catch (e: ConflictException) {
        // not transient - stop
        throw e
    } catch (e: QueryException) {
        lastError = e
        // sleep and retry
    }
}
if (lastError != null) throw lastError

Native-mode failure

NativeDB throws QueryException for any non-zero FFI return code. The message includes the operation name, the rc, and the string from mongreldb_kit_last_error():

try {
    NativeDB.create("/tmp/db", schema).use { ndb ->
        ndb.sqlRows("SELECT no_such_col FROM t")
    }
} catch (e: QueryException) {
    // sqlRows failed (rc=...): <ffi error message>
    System.err.println(e.message)
}

A null handle from open/create throws IllegalStateException via checkNotNull, carrying the last FFI error.

Quick reference

import com.visorcraft.mongreldb.*

// Category checks:
try { db.count("t") }
catch (e: NotFoundException)   { /* 404 */ }
catch (e: ConflictException)   { /* 409 - e.code, e.opIndex */ }
catch (e: AuthException)       { /* 401/403 */ }
catch (e: QueryException)      { /* 400/5xx/transport */ }
catch (e: MongrelDBException)  { /* catch-all parent */ }

// Conflict detail:
//   e.code     : String?  - "UNIQUE_VIOLATION" | "FK_VIOLATION" | ...
//   e.opIndex  : Int?     - index of the offending op in the batch

Next steps