Error Handling

July 10, 2026 ยท View on GitHub

The V client surfaces failures as values of a single, small MongrelError enum rather than as exceptions. Each variant maps to a category of HTTP or transport failure, so you discriminate with a match and recover precisely.

import mongreldb

The error type

Every public function on the client returns !T (a V option-result), where the error is a MongrelError:

VariantMeaning
http_error(string)A transport error or a server status we do not map more narrowly (3xx and most 5xx).
json_error(string)The server returned a malformed or unexpected JSON body.
authAuthentication or authorization failed (HTTP 401 or 403).
not_foundThe 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).
response_too_largeThe response body exceeded max_response_bytes.
already_committedA Transaction method was called after commit or rollback.

How HTTP status maps to an error

HTTP statusError
200 / 2xx(success, no error)
401, 403auth
404not_found
402, 409conflict
400 and other 4xxquery
3xx, 5xx, and transport failureshttp_error
Body that is not valid JSONjson_error

Matching errors

Use a match to handle each case:

db.schema_for('users') or {
	match err {
		mongreldb.MongrelError{...auth} { println('invalid credentials') }
		mongreldb.MongrelError{...not_found} { println('table missing') }
		mongreldb.MongrelError{...query} { println('malformed query') }
		else { println('other error') }
	}
	return
}

For the common "log and propagate" shape, a plain or { panic(err) } is enough:

rows := db.query('users').execute() or { panic(err) }

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 MongrelError.conflict.

mut txn := db.begin()
txn = txn.txn_put('orders', [mongreldb.Cell{1, mongreldb.int_value(10)}], false) or { panic(err) }

_ = txn.commit('order-batch-001') or {
	match err {
		mongreldb.MongrelError{...conflict} { println('batch rolled back - fix the data and retry') }
		else { panic(err) }
	}
}

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 MongrelError.already_committed. Start a new transaction for each batch.

Retries and idempotency

Network glitches and daemon restarts happen. Pair an idempotency key with a retry loop for commit. Only retry on MongrelError.http_error(...) (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 or {} discards the category and hides bugs. Match with match 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 already_committed. A transaction is single-use. If you share one across function boundaries, make it obvious who calls commit or rollback.

Next steps