Error Handling

July 10, 2026 ยท View on GitHub

The Gleam client surfaces failures as values of a single, small custom type rather than as exceptions. Each variant maps to a category of HTTP or transport failure, so you discriminate with a case and recover precisely.

import mongreldb

The error type

Every public function on the client returns Result(T, MongrelError), where MongrelError is this type:

VariantMeaning
Http(String)A transport error or a server status we do not map more narrowly (3xx and most 5xx).
Json(String)The server returned a malformed or unexpected JSON body.
AuthAuthentication or authorization failed (HTTP 401 or 403).
NotFoundThe table or row does not exist (HTTP 404).
ConflictA constraint violation rolled back a transaction, or a payment-required response (HTTP 402 or 409).
Query(String)The request was malformed: a bad condition, projection, or SQL statement (HTTP 400 and other 4xx).
ResponseTooLargeThe response body exceeded max_response_bytes.
AlreadyCommittedA Transaction method was called after commit or rollback.

Because Gleam custom types are exhaustive, you cannot accidentally forget a case once you write a case over them.

How HTTP status maps to an error

The client converts the daemon's response into a variant:

HTTP statusError
200 / 2xx(success, no error)
401, 403Auth
404NotFound
402, 409Conflict
400 and other 4xxQuery
3xx, 5xx, and transport failuresHttp
Body that is not valid JSONJson

Malformed JSON (a truncated body, a missing field the decoder expects) is reported as Json regardless of the HTTP status.

Matching errors

Use a case to handle each case:

case mongreldb.schema_for(db, "users") {
  Ok(desc) -> io.println("found")
  Error(mongreldb.Auth) -> io.println("invalid credentials")
  Error(mongreldb.NotFound) -> io.println("table missing")
  Error(mongreldb.Query(_)) -> io.println("malformed query")
  Error(_) -> io.println("other error")
}

For the common "log and propagate" shape, a plain let assert or use ... <- is enough:

let assert Ok(rows) = mongreldb.execute(mongreldb.query(db, "users"))

Transaction conflicts

A commit runs all staged ops in a single atomic batch. If any op violates a unique, foreign-key, check, or trigger constraint, the daemon rolls back the entire batch and returns HTTP 409, which the client surfaces as Error(Conflict).

let txn = mongreldb.begin(db)
let assert Ok(txn) = mongreldb.txn_put(txn, "orders", [mongreldb.Cell(1, mongreldb.int_value(10))], False)

case mongreldb.commit(txn, "order-batch-001") {
  Ok(_) -> io.println("committed")
  Error(mongreldb.Conflict) -> io.println("batch rolled back - fix the data and retry")
  Error(e) -> panic as string.inspect(e)
}

The idempotency key makes a safe retry possible: re-stage the same ops on a fresh transaction and commit with the same key. The daemon returns the original response on duplicate commits.

Single-use transactions

commit and rollback both flip an internal flag. Calling any method on the transaction afterward returns Error(AlreadyCommitted). Start a new transaction for each batch:

let txn = mongreldb.begin(db)
let assert Ok(_) = mongreldb.commit(txn, "key-1")

// reuse is an error:
// case mongreldb.txn_put(txn, "orders", [...], False) {
//   Error(mongreldb.AlreadyCommitted) -> ...
// }

let next = mongreldb.begin(db)
let assert Ok(_) = mongreldb.commit(next, "key-2")

Retries and idempotency

Network glitches and daemon restarts happen. Pair an idempotency key with a retry loop for commit. Only retry on Error(Http(...)) (transport) with the same idempotency key. Conflict and Query indicate a problem with the request itself and must be fixed before retrying.

Common pitfalls

Swallowing errors with a catch-all. A bare let _ = result discards the category and hides bugs. Match with case so each branch is explicit.

Retrying Conflict. A conflict means the batch violated a constraint; replaying the same ops will fail the same way. Fix the offending op, then retry.

Forgetting AlreadyCommitted. A transaction is single-use. If you share one across function boundaries, make it obvious who calls commit or rollback.

Next steps