1// Runtime error taxonomy for the Prisma ORM v8 Effect client. Worker-safe:
2// imports nothing beyond effect, and detects Prisma's error shapes
3// structurally (via their stable `kind` / `code` / `sqlState` discriminants)
4// so this module never has to value-import the optional peer.
5//
6// Granularity policy: a dedicated tag wherever the discriminant is STABLE
7// and handling genuinely branches — the SQL-standard integrity-violation
8// SQLSTATEs (23xxx) and Prisma's error categories (`ORM.*`,
9// `RUNTIME.*`, ...). Individual codes stay a typed `code` field rather than
10// one class per code: the code set churns per RC, while categories and
11// SQLSTATEs do not.
12import * as Data from "effect/Data";
13
14/**
15 * Prisma structured error codes observed in 8.0.0-rc.8 — an OPEN union
16 * (`string` stays assignable) so new RC codes never break, while known ones
17 * autocomplete.
18 */
19export type ErrorCode =
20 | "ORM.TABLE_UNKNOWN"
21 | "ORM.COLUMN_UNKNOWN"
22 | "ORM.RELATION_UNKNOWN"
23 | "ORM.INCLUDE_INVALID"
24 | "ORM.AGGREGATE_UNSUPPORTED"
25 | "ORM.MUTATION_ROW_MISSING"
26 | "ORM.CAPABILITY_MISSING"
27 | "RUNTIME.ENCODE_FAILED"
28 | "RUNTIME.DECODE_FAILED"
29 | "RUNTIME.ABORTED"
30 | "RUNTIME.CODEC_MISSING"
31 | "RUNTIME.PREPARE_UNUSED_PARAM"
32 | "RUNTIME.PREPARE_BIND_ON_ADHOC"
33 | "RUNTIME.TRANSACTION_CLOSED"
34 | "RUNTIME.TRANSACTION_COMMIT_FAILED"
35 | "RUNTIME.TRANSACTION_ROLLBACK_FAILED"
36 | "RUNTIME.BINDING_INVALID"
37 | "RUNTIME.BINDING_MISSING"
38 | "BUDGET.ROWS_EXCEEDED"
39 | "DRIVER.NOT_CONNECTED"
40 | "DRIVER.ALREADY_CONNECTED"
41 | "DRIVER.PREPARE_FAILED"
42 | "CONTRACT.MARKER_MISSING"
43 | "CONTRACT.MARKER_MISMATCH"
44 | "CONTRACT.VERIFY_FAILED"
45 | (string & {});
46
47interface QueryErrorFields {
48 message: string;
49 sqlState?: string | undefined;
50 constraint?: string | undefined;
51 table?: string | undefined;
52 column?: string | undefined;
53 detail?: string | undefined;
54 cause: unknown;
55}
56
57// ── integrity violations (SQL-standard SQLSTATE class 23) ─────────────
58// The driver normalizes target codes onto SQLSTATE, so these tags are
59// stable across databases and RCs.
60
61/**
62 * Unique or primary-key conflict (SQLSTATE `23505`) — the "email already
63 * registered" case:
64 *
65 * ```typescript
66 * Effect.catchTag("Prisma.UniqueViolationError", () =>
67 * Effect.succeed("already registered"),
68 * )
69 * ```
70 */
71export class UniqueViolationError extends Data.TaggedError(
72 "Prisma.UniqueViolationError",
73)<QueryErrorFields> {}
74
75/** Foreign-key violation (SQLSTATE `23503`) — a referenced row is missing. */
76export class ForeignKeyViolationError extends Data.TaggedError(
77 "Prisma.ForeignKeyViolationError",
78)<QueryErrorFields> {}
79
80/** NOT NULL violation (SQLSTATE `23502`). */
81export class NotNullViolationError extends Data.TaggedError(
82 "Prisma.NotNullViolationError",
83)<QueryErrorFields> {}
84
85/** CHECK constraint violation (SQLSTATE `23514`). */
86export class CheckViolationError extends Data.TaggedError(
87 "Prisma.CheckViolationError",
88)<QueryErrorFields> {}
89
90/** Every integrity-constraint tag (SQLSTATE class 23). */
91export type ConstraintViolationError =
92 | UniqueViolationError
93 | ForeignKeyViolationError
94 | NotNullViolationError
95 | CheckViolationError;
96
97// ── other SQL / connection failures ───────────────────────────────────
98
99/**
100 * Any other SQL statement failure (syntax, permissions, serialization,
101 * ...). `sqlState` carries the driver-normalized SQLSTATE for finer
102 * refinement.
103 */
104export class QueryError extends Data.TaggedError(
105 "Prisma.QueryError",
106)<QueryErrorFields> {}
107
108/**
109 * A connection-level failure (refused, reset, timed out). `transient`
110 * carries the driver's own judgment of retryability:
111 *
112 * ```typescript
113 * Effect.retry(effect, {
114 * while: (e) => e._tag === "Prisma.ConnectionError" && e.transient === true,
115 * times: 3,
116 * })
117 * ```
118 */
119export class ConnectionError extends Data.TaggedError(
120 "Prisma.ConnectionError",
121)<{
122 message: string;
123 transient?: boolean | undefined;
124 cause: unknown;
125}> {}
126
127// ── Prisma pipeline categories ────────────────────────────────────────
128
129/**
130 * ORM-lane misuse or validation failure (`ORM.*` codes: unknown
131 * table/column/relation, invalid include, unsupported aggregate, ...).
132 * Almost always a programming bug — don't retry.
133 */
134export class OrmError extends Data.TaggedError("Prisma.OrmError")<{
135 code: ErrorCode;
136 message: string;
137 cause: unknown;
138}> {}
139
140/**
141 * Execution-pipeline failure (`RUNTIME.*`, `DRIVER.*`, `BUDGET.*`,
142 * `CONTRACT.*`, `PLAN.*`, `LINT.*`, `MIGRATION.*` codes: encode/decode,
143 * codec registry, transaction lifecycle, row budgets, marker
144 * verification, ...). `code` pins the exact failure.
145 */
146export class RuntimeError extends Data.TaggedError("Prisma.RuntimeError")<{
147 code: ErrorCode;
148 message: string;
149 cause: unknown;
150}> {}
151
152/** A failure Prisma did not classify at all (no kind, no code). */
153export class UnknownError extends Data.TaggedError("Prisma.UnknownError")<{
154 code?: ErrorCode | undefined;
155 message: string;
156 cause: unknown;
157}> {}
158
159/**
160 * Deliberate transaction rollback. Yield `tx.rollback()` inside
161 * `db.transaction` to abort: the transaction rolls back and this error
162 * surfaces in the caller's error channel, catchable with
163 * `Effect.catchTag("Prisma.RollbackError", ...)`.
164 */
165export class RollbackError extends Data.TaggedError(
166 "Prisma.RollbackError",
167)<{}> {}
168
169/** Everything a Prisma query can fail with. */
170export type ClientError =
171 | ConstraintViolationError
172 | QueryError
173 | ConnectionError
174 | OrmError
175 | RuntimeError
176 | UnknownError;
177
178const field = (value: unknown, key: string): string | undefined => {
179 if (typeof value !== "object" || value === null) return undefined;
180 const v = (value as Record<string, unknown>)[key];
181 return typeof v === "string" ? v : undefined;
182};
183
184const message = (cause: unknown): string =>
185 cause instanceof Error ? cause.message : String(cause);
186
187/**
188 * The single funnel converting Prisma's thrown errors into the typed
189 * taxonomy. Detection uses stable discriminants: `kind: 'sql_query' |
190 * 'sql_connection'` on driver errors (with SQLSTATE refining the class-23
191 * integrity violations onto their own tags), and the `CATEGORY.NAME`
192 * structured `code` on ORM/pipeline errors.
193 */
194export const wrapPrismaError = (cause: unknown): ClientError => {
195 const kind = field(cause, "kind");
196 if (kind === "sql_query") {
197 const fields: QueryErrorFields = {
198 message: message(cause),
199 sqlState: field(cause, "sqlState"),
200 constraint: field(cause, "constraint"),
201 table: field(cause, "table"),
202 column: field(cause, "column"),
203 detail: field(cause, "detail"),
204 cause,
205 };
206 switch (fields.sqlState) {
207 case "23505":
208 return new UniqueViolationError(fields);
209 case "23503":
210 return new ForeignKeyViolationError(fields);
211 case "23502":
212 return new NotNullViolationError(fields);
213 case "23514":
214 return new CheckViolationError(fields);
215 default:
216 return new QueryError(fields);
217 }
218 }
219 if (kind === "sql_connection") {
220 const transient = (cause as { transient?: unknown }).transient;
221 return new ConnectionError({
222 message: message(cause),
223 transient: typeof transient === "boolean" ? transient : undefined,
224 cause,
225 });
226 }
227 const code = field(cause, "code");
228 if (code !== undefined) {
229 return code.startsWith("ORM.")
230 ? new OrmError({ code, message: message(cause), cause })
231 : new RuntimeError({ code, message: message(cause), cause });
232 }
233 return new UnknownError({
234 code: undefined,
235 message: message(cause),
236 cause,
237 });
238};