# Error helpers

> BetterDrizzleError, its error codes, and cross-dialect constraint detection helpers.

URL: https://better-drizzle.com/docs/reference/errors

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](/docs/advanced/error-handling) for end-to-end usage.

```ts
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` |

```ts
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:

```ts
const info = getDatabaseErrorInfo(error);
```

```js
{
  	driver: {
  		description: 'Detected driver.',
  		type: '"pg" | "sqlite" | "mysql" | "unknown"',
  	},
  	code: {
  		description: 'Driver-specific error code (e.g. "23505" on PostgreSQL).',
  		type: 'string | undefined',
  	},
  	errno: {
  		description: 'MySQL errno, when present.',
  		type: 'number | undefined',
  	},
  	constraint: {
  		description: 'Constraint name, when detectable.',
  		type: 'string | undefined',
  	},
  	table: {
  		description: 'Table involved, when detectable.',
  		type: 'string | undefined',
  	},
  	column: {
  		description: 'Column involved, when detectable.',
  		type: 'string | undefined',
  	},
  	message: {
  		description: 'The original error message.',
  		type: 'string',
  	},
  }
```

## `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.

```ts
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

```js
{
  	code: {
  		description: 'Error category. See the code table below.',
  		type: 'BetterDrizzleErrorCode',
  		required: true,
  	},
  	status: {
  		description: 'HTTP-like status. Derived from `code` unless set explicitly.',
  		type: 'number',
  		required: true,
  	},
  	message: {
  		description: 'Human-readable message.',
  		type: 'string',
  		required: true,
  	},
  	driver: {
  		description: 'Detected driver, when the error came from the database.',
  		type: '"pg" | "sqlite" | "mysql" | "unknown" | undefined',
  	},
  	dialect: {
  		description: 'SQL dialect in use, when known.',
  		type: 'string | undefined',
  	},
  	table: {
  		description: 'Table involved, when detectable.',
  		type: 'string | undefined',
  	},
  	column: {
  		description: 'Column involved, when detectable.',
  		type: 'string | undefined',
  	},
  	constraint: {
  		description: 'Violated constraint, when detectable.',
  		type: 'string | undefined',
  	},
  	operation: {
  		description: 'Operation that failed, for example `"create"` or `"transaction"`.',
  		type: 'string | undefined',
  	},
  	sqlState: {
  		description: 'SQLSTATE or driver error code from the database.',
  		type: 'string | undefined',
  	},
  	errno: {
  		description: 'Driver errno (MySQL), when present.',
  		type: 'number | undefined',
  	},
  	stage: {
  		description: 'Lifecycle stage where the error occurred.',
  		type: 'string | undefined',
  	},
  	hookName: {
  		description: 'Name of the hook that failed, for hook errors.',
  		type: 'string | undefined',
  	},
  	details: {
  		description: 'Extra structured data, for example `{ database: DatabaseErrorInfo }` or the offending values.',
  		type: 'Record<string, unknown> | undefined',
  	},
  	cause: {
  		description: 'The original error, when one was wrapped.',
  		type: 'unknown',
  	},
  }
```

### 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`.

```ts
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](/docs/plugins/tenant#missing-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](/docs/advanced/transactions) 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:

```ts
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'
	}
}
```
