Skip to content

feat: typed constraint errors + onError config hook (#376)#5877

Open
edisonmoy wants to merge 2 commits into
drizzle-team:mainfrom
edisonmoy:feat/376-typed-query-errors
Open

feat: typed constraint errors + onError config hook (#376)#5877
edisonmoy wants to merge 2 commits into
drizzle-team:mainfrom
edisonmoy:feat/376-typed-query-errors

Conversation

@edisonmoy

Copy link
Copy Markdown

Summary

Addresses #376 — the long-standing request for a type-safe, driver-independent way to react to query failures (unique / not-null / FK / check violations) instead of hand-parsing each driver's error codes/messages.

Two additive, fully backward-compatible changes:

1. Typed constraint errors

New classes under drizzle-orm, all extending the existing DrizzleQueryError (each with its own entityKind, so is() works across module/realm boundaries and the repo's require-entity-kind lint rule passes):

  • DrizzleConstraintError (base) with a kind: 'unique' | 'not_null' | 'foreign_key' | 'check' discriminant and best-effort constraintName / table / columns
  • UniqueConstraintError, NotNullConstraintError, ForeignKeyConstraintError, CheckConstraintError

The original driver error is preserved on .cause.

import { is, UniqueConstraintError } from 'drizzle-orm';

try {
  await db.insert(users).values({ email });
} catch (e) {
  if (is(e, UniqueConstraintError)) {
    e.constraintName; // e.g. 'users_email_unique'
    e.cause;          // original driver error
  }
}
// `e instanceof UniqueConstraintError` and `switch (e.kind)` also work.

Dialect-specific parsing (wrapPgError / wrapMySqlError / wrapSqliteError) lives in errors.ts and is invoked at the abstract dialect-core prepared query (pg-core / mysql-core / sqlite-core / singlestore-core). Because every concrete driver routes failures through those shared catch sites, all drivers of a dialect get typed errors with no per-driver changes. Classification is conservative — anything unrecognized falls back to a plain DrizzleQueryError. (Gel/EdgeDB keeps DrizzleQueryError; its error taxonomy isn't SQLSTATE-based.)

2. onError config hook

Optional onError?: (error: DrizzleQueryError) => void on DrizzleConfig, called with the wrapped error before it is thrown. It is threaded from drizzle() through every driver's session into prepared queries via a small core Session.attachErrorHandler helper, so it fires consistently across all drivers (pg, mysql, sqlite, singlestore, gel) — including inside transactions.

const db = drizzle(client, { onError: (err) => logger.warn(err.cause) });

Test plan

  • New drizzle-orm/tests/errors.test.ts: pg/mysql/sqlite/singlestore error classification, postgres.js name aliases, composite sqlite columns, libsql message fallback, is()/instanceof/inheritance chain, fallback-to-DrizzleQueryError, and an onError mock test.
  • Full drizzle-orm unit suite passes (588 tests).
  • tsc typecheck clean, ESLint clean (no disables, entityKind/no-instanceof satisfied), dprint clean.
  • Integration tests against live databases (cannot run in this environment — would appreciate a CI run).

Notes

  • No breaking changes: all new errors are subclasses of DrizzleQueryError, and onError is optional.

Made with Cursor

edisonmoy and others added 2 commits June 11, 2026 21:11
Add DrizzleConstraintError + Unique/NotNull/ForeignKey/Check subclasses
(extending DrizzleQueryError, each with its own entityKind so is() works)
and dialect wrap* parsers. Wire them into the pg/mysql/sqlite/singlestore
core prepared queries so all drivers of each dialect get typed errors.

Closes drizzle-team#376

Co-authored-by: Cursor <cursoragent@cursor.com>
Add an optional `onError(error: DrizzleQueryError)` callback to
DrizzleConfig, invoked with the wrapped error before it is thrown.
Threaded from drizzle() config through every driver's session into the
prepared queries via a core Session.attachErrorHandler helper, so it
fires consistently (including inside transactions) for pg, mysql,
sqlite, singlestore and gel drivers.

Refs drizzle-team#376

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant