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. |
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. |
| Operations | OPERATION_ERROR | 500 | An operation received invalid input or failed, for example an invalid batchSize or a bad cursor. |
| Operations | UNKNOWN | 500 | An uncategorized error. |
| 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. |
| 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'
}
}