Errors and exceptions

July 9, 2026 · View on GitHub

The C++ client reports failures by throwing. Every exception the client throws derives from MongrelDBException, so a single catch clause is enough to handle any client-originated failure. For finer-grained handling, catch the specific subclass.


The exception hierarchy

MongrelDBException              // base - any client failure
├── AuthException               // 401 or 403 - bad creds or forbidden
├── NotFoundException           // 404 - table, user, or role missing
├── ConflictException           // 409 - idempotency mismatch, etc.
└── QueryException              // 400 or 5xx - bad request or server error

Each subclass exposes what() (inherited from std::exception) for a human-readable message and, where relevant, the HTTP status code that produced it.

ClassWhen it is thrownTypical HTTP status
MongrelDBExceptionAny client failure (base class)-
AuthExceptionBad credentials or insufficient privilege401, 403
NotFoundExceptionReferenced table, user, or role does not exist404
ConflictExceptionIdempotency key mismatch, version conflict409
QueryExceptionMalformed request, SQL error, or server-side failure400, 5xx

Network failures (libcurl could not reach the server, timed out, etc.) and JSON decode failures are also thrown as MongrelDBException. There is no separate network exception subclass - if you need to distinguish them, check the message or wrap the call.

Catching by base class

For most application code, catching the base class is enough:

try {
    db.put("orders", {{"id", mongreldb::Value::integer(1)}}, mongreldb::OpType::Put);
    auto res = db.query("orders", {cond});
} catch (const mongreldb::MongrelDBException &e) {
    std::cerr << "mongreldb error: " << e.what() << "\n";
}

Catching by subclass

Handle the cases you can recover from, and let the rest bubble up:

try {
    auto res = db.query("missing_table", {cond});
} catch (const mongreldb::NotFoundException &e) {
    // Table does not exist. Create it and retry.
    create_orders_table(db);
    auto res = db.query("missing_table", {cond});
} catch (const mongreldb::AuthException &e) {
    // Token expired or lacks privilege. Surface to the caller.
    throw;
} catch (const mongreldb::MongrelDBException &e) {
    // Anything else.
    std::cerr << "error: " << e.what() << "\n";
    throw;
}

The order of catch clauses matters: subclasses first, base last.

HTTP status mapping

The client maps the server's HTTP response code to an exception class:

  • 2xx - success, no exception.
  • 401 / 403 - AuthException.
  • 404 - NotFoundException.
  • 409 - ConflictException.
  • 400 and 5xx - QueryException. The message includes the server's error text when the body is an error envelope.
  • Any other non-2xx code falls through to QueryException.

The server's error envelope (when present) is a small JSON object with a message and sometimes a code. The client extracts the message and puts it into what(), so the most useful detail surfaces without parsing.

Conflict vs overwrite

MongrelDB treats a Put with an existing primary key as an overwrite, not a conflict. You will not see a ConflictException from a duplicate-key Put. ConflictException is reserved for genuine version conflicts - for example, an idempotency key that does not match the one stored with the row.

If you need insert-if-absent semantics, use the idempotency key: the first Put with a given key wins, and a later Put with the same key is treated as a safe retry rather than an overwrite.

Idempotent retries

Network glitches and redeploys should be safe to retry. Attach an idempotency key to any mutating call, and the server will deduplicate retries within its retention window:

db.put("orders", row, mongreldb::OpType::Put, "client-uuid-1234");

A retry with the same key returns the original result instead of creating a second row. If the second attempt carries a different row body, the server raises a conflict (409) and the client throws ConflictException.

RAII and resource safety

The client owns its libcurl handle and buffers behind a unique_ptr pimpl. Destroying a client (or letting it go out of scope) frees everything. A client is movable but not copyable - pass it by reference, store it by value, or move it into place. Exceptions thrown mid-operation do not leak handles.

A complete example

#include <mongreldb/mongreldb.hpp>
#include <iostream>

int main() {
    mongreldb::MongrelDBClient db(
        "http://localhost:8080", mongreldb::Auth::none());

    try {
        mongreldb::Condition pk;
        pk.kind = mongreldb::CondKind::Pk;
        pk.int_value = 42;
        auto res = db.query("orders", {pk});

        for (const auto &row : res.rows) {
            for (const auto &cell : row) {
                std::cout << "col " << cell.column_id << "\n";
            }
        }
    } catch (const mongreldb::NotFoundException &e) {
        std::cerr << "not found: " << e.what() << "\n";
        return 1;
    } catch (const mongreldb::MongrelDBException &e) {
        std::cerr << "error: " << e.what() << "\n";
        return 1;
    }
    return 0;
}