Skip to content
result-rpc
Esc
navigateopen⌘Jpreview
On this page

Database errors

Fold database constraint failures into declared domain tags.

Database error handling is no longer part of result-rpc. It lives in db-result, a driver-agnostic Result boundary built on better-result that classifies every driver failure into a db/* tagged error (constraints, contention, connection, syntax — same vocabulary across pg, SQLite, D1, mysql, mssql, Prisma, Kysely, and Drizzle) and retries only what is provably safe.

import { matchErrorPartial } from "better-result";
import { tryDb } from "db-result/sqlite"; // or /pg /mysql2 /mssql /d1

const inserted = await tryDb(db.insert(reviews).values(row).returning());
if (inserted.status === "error") {
  return matchErrorPartial(
    inserted.error,
    {
      "db/unique-violation": () => err(errors.alreadyReviewed({ hotelId })),
    },
    (unhandled) => {
      throw unhandled;
    },
  );
}

Attempting the insert is the uniqueness check. Unlike a SELECT-first check, the constraint remains correct when two requests race.

The boundary folds the lane

The db/* tags are private composition currency, not wire errors: fold them into your procedure’s declared domain errors at the handler boundary. result-rpc’s contract rejects any undeclared error lane, so an uncollapsed database error is sanitized to server/internal rather than leaking driver details. The fold is mechanical — matchErrorPartial lists the tags you handle, and its terminal arm is typed as the remainder the compiler lists for you.

Was this page helpful?