Error helpers
BetterDrizzleError, its error codes, and cross-dialect constraint detection helpers.
better-drizzle exports helpers that normalize database errors across PostgreSQL, SQLite, and MySQL, so you can classify failures without string-matching driver messages. See error handling for end-to-end usage.
import {
isDatabaseError,
getDatabaseErrorInfo,
isUniqueViolation,
isForeignKeyViolation,
isNotNullViolation,
isCheckViolation,
} from 'better-drizzle';Predicates
Each returns a boolean and accepts an optional constraint name (or column name, for isNotNullViolation) to match a specific one.
| Function | Signature |
|---|---|
isDatabaseError | (error: unknown) => boolean |
isUniqueViolation | (error: unknown, constraint?: string) => boolean |
isForeignKeyViolation | (error: unknown, constraint?: string) => boolean |
isNotNullViolation | (error: unknown, column?: string) => boolean |
isCheckViolation | (error: unknown, constraint?: string) => boolean |
try {
await client.users.create({ data });
} catch (error) {
if (isUniqueViolation(error, 'users_email_unique')) {
// handle duplicate email specifically
}
throw error;
}getDatabaseErrorInfo
Parses any caught error into a normalized object, regardless of driver:
const info = getDatabaseErrorInfo(error);Prop
Type
BetterDrizzleError
Every error thrown by better-drizzle itself is a BetterDrizzleError (it extends Error). Database failures that surface through an operation are normalized into the same shape.
import { BetterDrizzleError, BetterDrizzleErrorCode } from 'better-drizzle';
try {
await client.users.findUnique({ where: { id: 1 } }).throw();
} catch (error) {
if (
BetterDrizzleError.is(error) &&
error.code === BetterDrizzleErrorCode.ResultNotFound
) {
// error.status === 404
}
throw error;
}Fields
Prop
Type
Static helpers
| Helper | Description |
|---|---|
BetterDrizzleError.is(error) | Type guard, same as error instanceof BetterDrizzleError. |
BetterDrizzleError.from(error, overrides?) | Normalizes any value. An existing BetterDrizzleError is cloned with the overrides applied. A value that looks like a database error (an object with a string message, code, or sqlState, or a numeric errno, which includes plain Error instances) becomes DATABASE_ERROR with the parsed driver, constraint, table, and column. Anything else becomes UNKNOWN. The original value is kept as cause. |
BetterDrizzleError.fromDatabaseError(error, overrides?) | Always parses the value with getDatabaseErrorInfo and returns DATABASE_ERROR (unless overrides.code is set), with the parsed info under details.database. |
Drizzle ORM 1.x wraps driver failures in DrizzleQueryError (message Failed query: ...) and keeps the driver error as its cause. BetterDrizzleError.from(...), fromDatabaseError(...), getDatabaseErrorInfo, and the isUniqueViolation / isForeignKeyViolation / isNotNullViolation / isCheckViolation helpers read that wrapped driver error, so sqlState, constraint, table, and column still resolve. When you inspect a raw Drizzle error yourself, read error.cause for the driver fields.
Instances also have withCause(cause) and withDetails(details), which return a new error (details are shallow-merged), and toJSON(), which returns a plain object with every field except cause.
try {
await db.insert(users).values(data);
} catch (error) {
throw BetterDrizzleError.fromDatabaseError(error, {
operation: 'create',
table: 'users',
});
}Error codes
BetterDrizzleErrorCode is an exported string enum. The status column is the default status for each code; any code not given a specific status defaults to 500.
| Area | Code | Status | Meaning |
|---|---|---|---|
| Results | RESULT_NOT_FOUND | 404 | .throw() found no matching row. |
| Results | REPOSITORY_NOT_FOUND | 500 | repository(name) matched no schema key or table name. |
| Database | DATABASE_ERROR | 500 | The database rejected the statement (unique, foreign key, not null, and so on). |
| Database | DIALECT_INFERENCE_FAILED | 500 | The SQL dialect could not be inferred from the Drizzle client. |
| Database | JSONB_QUERY_UNSUPPORTED | 400 | A JSONB path filter was used on a dialect other than PostgreSQL. |
| Database | JSONB_MUTATION_UNSUPPORTED | 400 | A JSONB path mutation (dotted path or { json } wrapper) was used on a dialect other than PostgreSQL. |
| Database | ARRAY_QUERY_UNSUPPORTED | 400 | A native array filter was used on a dialect other than PostgreSQL. |
| Database | ARRAY_MUTATION_UNSUPPORTED | 400 | A native array mutation (append, prepend, remove, replace, addUnique) was used on a dialect other than PostgreSQL. |
| Operations | UNIQUE_WHERE_REQUIRED | 400 | A findUnique where does not pin one row by its primary key or a unique key (for example { id: undefined }). |
| Operations | INVALID_ARGS | 400 | The call's arguments are invalid: a bad shape, conflicting options (page with skip, before with after, select with include), an unknown column or relation, or an invalid value such as a non-positive batchSize, a non-finite atomic operand, or overlapping JSONB paths. Thrown before any SQL. |
| Operations | OPERATION_ERROR | 500 | An operation failed for another reason, for example a relation selector matching no row or several rows, a feature the dialect does not support without a more specific code, or a non-library error wrapped by a hook. |
| Operations | UNKNOWN | 500 | An uncategorized error. |
| Prepared statements | PREPARED_PARAM_MISSING | 400 | execute() received no value for a param(). |
| Prepared statements | PREPARED_PARAM_UNKNOWN | 400 | execute() received a value for a name the statement does not use. |
| Prepared statements | PREPARED_UNSUPPORTED | 400 | The read cannot be prepared: a list param outside PostgreSQL, or a param inside a relation's include / select args. |
| Hooks | HOOK_ERROR | 500 | A lifecycle hook threw. |
| Plugins | PLUGIN_DIALECT_UNSUPPORTED | 500 | A plugin does not support the current dialect. |
| Plugins | PLUGIN_DUPLICATE_ID | 500 | Two plugins share the same id. |
| Plugins | PLUGIN_EXTENSION_CONFLICT | 500 | Two plugins or extensions add the same client property. |
| Plugins | PLUGIN_OPERATION_ARG_CONFLICT | 500 | Two plugins add the same operation arg field. |
| Plugins | PLUGIN_REQUIRED_COLUMN_MISSING | 500 | A model lacks a column a plugin requires. |
| Plugins | PLUGIN_REQUIRED_COLUMN_TYPE | 500 | A required column exists with a different Drizzle type. |
| Tenant | TENANT_REQUIRED | 403 | A scoped model ran without a tenant value for one of its columns, or an inherited junction was used directly. details.columns lists the columns. See tenant. |
| Tenant | TENANT_MISMATCH | 403 | A write set a tenant column to another tenant's value, or a PostgreSQL/SQLite upsert conflicted with another tenant's row. |
| Tenant | TENANT_RAW_DENIED | 403 | Raw SQL ran on a tenant-scoped client under raw: 'deny' without { tenant: 'trusted' }. |
| Tenant | TENANT_ALREADY_SCOPED | 400 | $tenant() set a column that the client already scopes. |
| Cache | CACHE_INVALID_OPTIONS | 500 | Thrown by cache() when store or ttl is missing, a duration is not a positive number of seconds or a '30s' / '5m' / '1h' / '1d' string, a ttl or negativeTtl (plugin, model, or per call) exceeds versionTtl, or maxSize is not a positive finite number. A per-call TTL is checked when the read runs. redis() throws it for a client without send, call, or sendCommand. |
| Cache | CACHE_STORE_ERROR | 500 | The store's get, set, or delete failed. Thrown by $cache.invalidate() and $cache.clear(). During reads and writes it is only passed to onError (details.stage: read, write, or invalidate); the read falls back to the database and the write still succeeds. |
| Cache | CACHE_SERIALIZATION_ERROR | 500 | A result could not be serialized, or a stored entry could not be deserialized. Never thrown: passed to onError (details.stage: serialize or deserialize); the result is returned uncached, or the read runs the query. |
| Cache | CACHE_VALUE_TOO_LARGE | 500 | A serialized entry is larger than maxSize. Never thrown: passed to onError with details.size and details.maxSize; the result is returned but not stored. |
| Raw SQL | RAW_DISABLED | 500 | Raw SQL is disabled with raw.enabled: false. |
| Raw SQL | RAW_UNSAFE_DISABLED | 500 | $rawUnsafe() was called without raw.allowUnsafe: true. |
| Raw SQL | RAW_COMMENT_REQUIRED | 400 | raw.requireComment is on and the call has no options.comment. |
| Raw SQL | RAW_INVALID_QUERY | 400 | $raw / $executeRaw got something other than a tagged template or Drizzle SQL object. |
| Raw SQL | RAW_UNSAFE_PLACEHOLDER_MISMATCH | 400 | The ? placeholder count does not match the params count. |
| Raw SQL | RAW_UNSUPPORTED_OPTION | 400 | A raw option is not supported by the dialect. |
| Raw SQL | RAW_TIMEOUT | 408 | A raw query (or .explain() with timeoutMs) timed out. |
| Raw SQL | RAW_ABORTED | 408 | A raw query was aborted through its AbortSignal. |
| Transactions | TRANSACTION_ROLLBACK | 409 | The transaction was rolled back with tx.rollback(). |
| Transactions | TRANSACTION_TIMEOUT | 408 | The transaction exceeded timeoutMs. |
| Transactions | TRANSACTION_ABORTED | 408 | The transaction was aborted through its AbortSignal. |
| Transactions | TRANSACTION_UNSUPPORTED_OPTION | 400 | A transaction option is not supported by the dialect. |
| Transactions | TRANSACTIONS_UNSUPPORTED | 500 | The Drizzle client does not support transactions. |
| Transactions | AFTER_COMMIT_OUTSIDE_TRANSACTION | 500 | afterCommit() was called outside a transaction. |
| Transactions | AFTER_ROLLBACK_OUTSIDE_TRANSACTION | 500 | afterRollback() was called outside a transaction. |
| Locks | LOCK_NOT_SUPPORTED | 400 | The dialect or query shape does not support the requested row lock. |
| Locks | LOCK_REQUIRES_TRANSACTION | 400 | locks.transactionsOnly: true is set and the locked read ran outside a transaction. |
| Locks | LOCK_TIMEOUT | 409 | The lock could not be acquired in time, including NOWAIT failures. |
| Internal | TABLE_RUNTIME_NOT_FOUND | 500 | Internal: table metadata is missing. |
| Internal | TRANSACTION_LIFECYCLE_STATE_MISSING | 500 | Internal: transaction lifecycle state is missing. |
| Internal | TRANSACTION_RUNTIME_NOT_INITIALIZED | 500 | Internal: the transaction runtime was not initialized. |
BetterDrizzleTransactionRollbackError
Thrown when a transaction is explicitly rolled back via tx.rollback(reason). It extends BetterDrizzleError with code TRANSACTION_ROLLBACK (status 409). The reason you pass is available as error.reason (also as cause and details.reason), and a non-empty string reason becomes the message:
import { BetterDrizzleTransactionRollbackError } from 'better-drizzle';
try {
await client.transaction(async (tx) => {
tx.rollback('duplicate email');
});
} catch (error) {
if (error instanceof BetterDrizzleTransactionRollbackError) {
console.log(error.reason); // 'duplicate email'
}
}