drizzle-orm
semver
>=0.45.0 <1.0.0postconditions15functions9last verified2026-06-12coverage score69%Postconditions — what we check
- select · select-query-errorerrorWhenquery fails (connection lost, timeout, invalid column, SQL syntax error)Throws
Error with database-specific error detailsRequired handlingCaller MUST wrap select queries in try-catch to handle database errors. Connection failures and SQL errors crash application if unhandled.costhighin prodimmediate exceptionusers seelost datavisibilityvisibleSources[1] - insert · insert-constraint-violationerrorWheninsert violates constraint (unique, foreign key, not null, check constraint)Throws
Error with constraint violation details and database error codeRequired handlingCaller MUST wrap insert operations in try-catch to handle constraint violations. Unique violations, foreign key errors, and validation failures crash application if unhandled.costhighin prodimmediate exceptionusers seelost datavisibilityvisibleSources[2] - update · update-constraint-violationerrorWhenupdate violates constraint or fails (unique, foreign key, connection lost)Throws
Error with constraint violation or connection error detailsRequired handlingCaller MUST wrap update operations in try-catch to handle constraint violations and connection errors. Database errors crash application if unhandled.costhighin prodimmediate exceptionusers seelost datavisibilityvisibleSources[3] - delete · delete-constraint-violationerrorWhendelete violates foreign key constraint (referenced by other tables)Throws
Error with foreign key constraint violation detailsRequired handlingCaller MUST wrap delete operations in try-catch to handle foreign key constraint violations. Cascade deletes and constraint errors crash application if unhandled.costhighin prodimmediate exceptionusers seelost datavisibilityvisibleSources[4] - transaction · transaction-rollback-errorerrorWhentx.rollback() is called inside the transaction callbackThrows
TransactionRollbackError (extends DrizzleError) with message 'Rollback'Required handlingCaller wrapping db.transaction() MUST catch TransactionRollbackError to distinguish intentional rollbacks (business logic rejected the transaction) from actual database failures. If TransactionRollbackError is swallowed silently, the caller has no way to tell whether the transaction committed or was intentionally aborted. Pattern: catch (err) { if (err instanceof TransactionRollbackError) { /* intentional */ } else { throw err; } }costhighin prodsilent failureusers seelost datavisibilitysilent - transaction · transaction-db-error-no-try-catcherrorWhendb.transaction() called without try-catch, and an operation inside throwsThrows
Database-specific error (e.g. PostgreSQL error code 23505, 23503, ECONNRESET) that propagates through transaction() after automatic rollbackRequired handlingCaller MUST wrap db.transaction() in try-catch. When any operation inside the callback throws, drizzle automatically rolls back the entire transaction and re-throws the original error. Without try-catch at the call site, the error propagates as an unhandled rejection, crashing the application while the transaction is correctly rolled back — but the caller has no chance to retry or return a meaningful error to the user.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[5] - transaction · transaction-nested-savepointwarningWhendb.transaction() called inside another transaction (nested transaction via PgTransaction.transaction())Throws
Uses SAVEPOINT semantics — inner rollback only affects savepoint, not outer transaction; outer tx.rollback() affects everythingRequired handlingCaller using nested transactions MUST understand savepoint semantics. A rollback in an inner transaction only rolls back to the savepoint, not the entire outer transaction. The outer transaction continues and MUST be explicitly committed or rolled back. Missing this distinction causes partial data writes that appear as correct commits to the application but only committed a subset of the intended work.costmediumin prodsilent failureusers seelost datavisibilitysilentSources[5] - execute · execute-query-errorerrorWhenraw SQL fails — syntax error, table not found, type mismatch, constraint violationThrows
DrizzleQueryError with message 'Failed query: <sql>\nparams: <params>', wrapping the original driver error as .causeRequired handlingCaller MUST wrap db.execute() in try-catch. DrizzleQueryError includes the executed query and params in the error message — NEVER log these unredacted to external services (they may contain PII). Check err.cause for the underlying PostgreSQL error code. Common codes: 42601 (syntax error), 42P01 (table not found), 23505 (unique violation), 23503 (foreign key violation).costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - execute · execute-type-mismatch-no-runtime-checkwarningWhensql<T> generic parameter does not match actual result shape at runtimeThrows
No error thrown — TypeScript type is a lie; runtime data is wrong shape silentlyRequired handlingCaller MUST NOT rely on TypeScript generic type T for runtime validation. Drizzle documentation explicitly states: 'sql<T> does not perform any runtime mapping.' If the SQL result does not match the expected shape, TypeScript will not catch this at runtime — you will get silent type mismatches causing undefined property access or wrong data returned to users. Validate with zod or manual shape checks after db.execute().costmediumin prodsilent failureusers seelost datavisibilitysilentSources[7] - findFirst · findfirst-undefined-on-missing-rowerrorWhendb.query.<table>.findFirst({ where: ... }) is called and the filter matches zero rows. The promise resolves with `undefined`, not a thrown error.Throws
No error thrown — returns undefined when no row matches the filterRequired handlingCaller MUST check the result for undefined before accessing any property. findFirst() returns `T | undefined` — the TypeScript type is honest, but a missing row looks identical to a successful lookup at runtime. Common anti-pattern: const user = await db.query.users.findFirst({ where: eq(users.id, id) }); return user.email; // TypeError if user is undefined Correct pattern: const user = await db.query.users.findFirst({ where: eq(users.id, id) }); if (!user) throw new NotFoundError(`User ${id} not found`); return user.email; This is the most common drizzle bug pattern in production — it surfaces as `Cannot read properties of undefined (reading 'X')` in request logs, usually triggered by stale IDs in URL params, deleted records, or permission-filtered lookups.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - findFirst · findfirst-driver-errorerrorWhendb.query.<table>.findFirst() is called and the underlying driver throws — connection lost (ECONNRESET, ETIMEDOUT), table missing (42P01), invalid relation config (the schema's `relations` object references a column that does not exist on the target table), or query syntax error in the generated SQL.Throws
Driver error (pg.DatabaseError / postgres.PostgresError / etc.) propagates as a Promise rejectionRequired handlingCaller MUST wrap db.query.<table>.findFirst() in try-catch. Connection failures during read paths crash the request handler. Invalid relations configs (a `with: { posts: true }` referencing a relation that was not declared in the schema) surface here at runtime, not at compile time — these are common after schema refactors where relations were removed but query call sites were not updated. The same try-catch should handle both the driver error and (separately) the undefined-row case covered by findfirst-undefined-on-missing-row.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[8] - findMany · findmany-driver-errorerrorWhendb.query.<table>.findMany() is called and the underlying driver throws — connection lost, table missing, invalid relation config in the `with:` option referencing an undefined relation, or query syntax error.Throws
Driver error (pg.DatabaseError / postgres.PostgresError / etc.) propagates as a Promise rejectionRequired handlingCaller MUST wrap db.query.<table>.findMany() in try-catch. Connection failures during list endpoints (paginated reads, dashboard data loads) crash the request handler. Invalid `with:` configs that reference removed/renamed relations surface here at runtime — they do not produce TypeScript errors because the schema's relations object is checked structurally and a missing key falls back to `unknown`. The error message includes the SQL fragment but NOT the variable name in the application code, so the call site is hard to locate without a stack trace.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[8] - findMany · findmany-empty-array-on-stale-filterwarningWhendb.query.<table>.findMany() is called with a `where:` filter that no rows satisfy (deleted records, wrong organization scope, stale enum value after a schema migration, etc.). The promise resolves with an empty array.Throws
No error thrown — returns [] silently when filter matches zero rowsRequired handlingCaller SHOULD distinguish empty-result-by-design from empty-result-by-mistake. For endpoints that paginate user data, an empty array is a valid response. For endpoints that expect at least one row (e.g. fetching an organization's billing records during a renewal job), the empty array is a silent indicator of misconfigured filters or stale state and MUST be checked: const rows = await db.query.invoices.findMany({ where: eq(invoices.orgId, orgId) }); if (rows.length === 0) { logger.warn('No invoices found for org', { orgId }); // decide: throw, fallback, or return early } Without this check, downstream code that aggregates or averages over the array silently produces zeros, NaN, or misleading dashboard metrics — a common drizzle production-data bug.costmediumin prodsilent failureusers seedegraded performancevisibilitysilentSources[8] - batch · batch-statement-failure-rolls-back-allerrorWhendb.batch([...]) is called and any statement in the batch fails (constraint violation, syntax error, connection drop mid-batch). The driver throws — the entire batch is rolled back, none of the statements committed.Throws
Driver-specific error from the failing statement (D1Error, LibsqlError, or NeonDbError). The error message names the failing statement but the typed BatchResponse you expected to receive is NEVER returned — the rejection replaces it.Required handlingCaller MUST wrap db.batch() in try-catch. The atomic semantics mean either all statements commit and you get the typed BatchResponse, or NONE commit and you get a rejection. Application code that builds on partial success (e.g. "if step 3 fails, statements 1 and 2 should still be applied") MUST NOT use batch() — use sequential awaits inside individual try-catch blocks instead. Common silent-failure pattern: developers assume batch() returns a per-statement result object indicating which statements succeeded. It does NOT — the success path returns one typed tuple, the failure path throws. Code that destructures the response without try-catch crashes the entire request handler on any single-row constraint violation buried in the batch.costhighin prodimmediate exceptionusers seelost datavisibilityvisibleSources[10] - batch · batch-unsupported-on-non-batchable-driverwarningWhendb.batch([...]) is called on a driver wrapper that does not implement batch — node-postgres (drizzle-orm/node-postgres), postgres-js (drizzle-orm/postgres-js), better-sqlite3, mysql2, planetscale. This occurs when shared code receives a db instance typed as a union or `any`, and the user passed the wrong driver.Throws
TypeError: db.batch is not a function (runtime) — TypeScript catches it if the type is precise, but `any`-typed code reaches runtimeRequired handlingCode that uses db.batch() MUST be invoked only with a D1, LibSQL, or Neon db instance. When writing shared utility functions, either parameterize the db type precisely (DrizzleD1Database, LibsqlDatabase, NeonHttpDatabase) or feature-detect: if (typeof db.batch === 'function') { await db.batch([...]); } else { // fallback to db.transaction() for pg/sqlite drivers } A surprising number of drizzle batch() crashes in production come from code that was written assuming D1 in dev/preview environments but ran against postgres-js in production.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[10]
Sources
Every postcondition cites at least one of these. Numbered to match the footnotes above.
- [1]orm.drizzle.team/docs/selecthttps://orm.drizzle.team/docs/select
- [2]orm.drizzle.team/docs/inserthttps://orm.drizzle.team/docs/insert
- [3]orm.drizzle.team/docs/updatehttps://orm.drizzle.team/docs/update
- [4]orm.drizzle.team/docs/deletehttps://orm.drizzle.team/docs/delete
- [5]orm.drizzle.team/docs/transactionshttps://orm.drizzle.team/docs/transactions
- [6]raw.githubusercontent.com/drizzle-team/drizzle-ormhttps://raw.githubusercontent.com/drizzle-team/drizzle-orm/main/drizzle-orm/src/errors.ts
- [7]orm.drizzle.team/docs/sqlhttps://orm.drizzle.team/docs/sql
- [8]orm.drizzle.team/docs/rqbhttps://orm.drizzle.team/docs/rqb
- [9]raw.githubusercontent.com/drizzle-team/drizzle-ormhttps://raw.githubusercontent.com/drizzle-team/drizzle-orm/main/drizzle-orm/src/pg-core/query-builders/query.ts
- [10]orm.drizzle.team/docs/batch-apihttps://orm.drizzle.team/docs/batch-api
Need a different package?
Request a profile