Error handling

July 10, 2026 ยท View on GitHub

Every non-2xx response from the daemon is mapped to a typed Crystal exception. This is the complete reference: the exception hierarchy, the HTTP-status mapping, the daemon's error envelope, and recovery patterns for each category.


The error model

All client errors descend from MongrelDB::MongrelDBError. The client raises a specific subclass for each failure category:

ClassMeaningTypical cause
MongrelDB::MongrelDBErrorBase class for all client errors(rescue this to catch any failure)
MongrelDB::AuthErrorHTTP 401 or 403Missing/bad credentials against an auth-enabled daemon
MongrelDB::NotFoundErrorHTTP 404Missing table, schema, or resource
MongrelDB::ConflictErrorHTTP 409Unique, foreign-key, check, or trigger violation at commit
MongrelDB::QueryErrorHTTP 400 or 5xx, plus networkMalformed request, server failure, transport error

ConflictError carries extra detail via accessors:

AccessorMeaning
ex.error_codeThe server's structured error code (e.g. "UNIQUE_VIOLATION"); "" when absent
ex.op_indexThe offending op index within a batch, when reported; nil otherwise

The daemon's error envelope

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

Structured codes you will commonly see in error_code:

error_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, 403AuthErrorBad/missing credentials
404NotFoundErrorResource not found
409ConflictErrorConstraint violation at commit
400QueryErrorMalformed request / bad query
5xxQueryErrorDaemon-side failure
other non-2xxQueryErrorCatch-all
2xx(no error)Success

Network and encoding problems (Socket::Error, IO::Error, JSON encode failures for NaN/Infinity, etc.) are also mapped to QueryError.

Discriminating errors

By category - rescue the subclass

begin
  db.schema_for("missing_table")
rescue ex : MongrelDB::NotFoundError
  puts "table does not exist"
rescue ex : MongrelDB::ConflictError
  puts "unexpected conflict on a read"
rescue ex : MongrelDB::AuthError
  puts "bad credentials"
rescue ex : MongrelDB::QueryError
  puts "server error or malformed request"
rescue ex : MongrelDB::MongrelDBError
  puts "other error: #{ex.message}"
end

By details - read ConflictError fields

begin
  txn.commit
rescue ex : MongrelDB::ConflictError
  puts "status=409 code=#{ex.error_code} op=#{ex.op_index} msg=#{ex.message}"
end

Recovery patterns

Auth failure - do not retry blindly

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

rescue ex : MongrelDB::AuthError
  raise "credentials rejected; refresh token: #{ex.message}"
end

Not found - fall back, do not crash

For lookups by primary key, a 404 may be a normal "absent" result.

begin
  db.schema_for(table_name)
rescue ex : MongrelDB::NotFoundError
  {} of String => JSON::Any # table missing - treat as empty
end

Note: a pk query against an existing table returns zero rows, not a 404; NotFoundError here means the table itself is missing.

Constraint conflict - report the offending op

begin
  txn.commit
rescue ex : MongrelDB::ConflictError
  if idx = ex.op_index
    STDERR.puts "op #{idx} violated #{ex.error_code}: #{ex.message}"
  else
    STDERR.puts "conflict #{ex.error_code}: #{ex.message}"
  end
  raise ex
end

The engine already rolled back the whole batch - there is nothing to undo.

Transient failure - retry with an idempotency key

QueryError covers transport and 5xx failures. With an idempotency key, retrying a transaction is safe (see transactions.md).

def run(db, build_txn, key)
  # build_txn is a Proc returning a fresh Transaction with the same ops.
  build_txn.call(db).commit(idempotency_key: key)
rescue ex : MongrelDB::AuthError | MongrelDB::ConflictError
  raise ex # not transient
rescue ex : MongrelDB::MongrelDBError
  raise ex # QueryError / network - caller may retry with the same key
end

Transaction-state error

Calling commit or rollback twice on the same Transaction raises Exception. That is a programming bug - fix the control flow rather than catching it.

Quick reference

# Category checks (most specific first):
rescue ex : MongrelDB::AuthError      # 401/403
rescue ex : MongrelDB::NotFoundError  # 404
rescue ex : MongrelDB::ConflictError  # 409
rescue ex : MongrelDB::QueryError     # 400/5xx/network
rescue ex : MongrelDB::MongrelDBError # base

# Detail extraction on a conflict:
rescue ex : MongrelDB::ConflictError
  ex.error_code # String, e.g. "UNIQUE_VIOLATION"
  ex.op_index   # Int32? or nil
  ex.message    # String
end

Next steps