drizzle-orm
semver
>=0.45.0 <1.0.0postconditions20functions11last verified2026-06-24coverage score73%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] - migrate · migrate-missing-journal-or-sql-fileerrorWhenmigrate(db, { migrationsFolder }) is called and either meta/_journal.json is missing from migrationsFolder, or the journal references a .sql file that does not exist on disk.Throws
Error with message "Can't find meta/_journal.json file" or "No file <path> found in <folder> folder"Required handlingCaller MUST wrap migrate() in try-catch. The fs-missing errors are generic Error instances (no custom class) — match on message text or rely on the absence of any other plausible failure surface at the call site. Surface these failures loudly (process exit / startup alert) — a server that boots without applying its migrations will throw schema errors on the first user request instead of failing fast.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - migrate · migrate-driver-error-rolls-back-pendingerrorWhenmigrate() is called and a SQL statement in one of the pending migrations fails — invalid DDL, constraint violation against existing data, lock timeout, connection drop, permission denied on the migrations schema.Throws
Driver-specific error (pg.DatabaseError / postgres.PostgresError / D1Error / LibsqlError / etc.) propagates as a Promise rejection. All pending migrations in this call are rolled back atomically.Required handlingCaller MUST wrap migrate() in try-catch and treat any rejection as fatal — the migrations table state will not advance, so the next process startup will retry the same migrations from the same point. Do NOT catch-and-continue: a server that proceeds past a migration failure runs against a partially-migrated schema and produces inconsistent reads/writes. Log the underlying error code (err.code or err.cause?.code) so on-call can distinguish data conflicts (require backfill before retry) from transient infrastructure errors (safe to retry).costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - migrate · migrate-no-built-in-lock-concurrent-startup-racewarningWhenmigrate() is called from multiple processes simultaneously against the same database (rolling deploys, multi-replica startup, CI parallelism) and a new migration is pending. The processes race and one of them sees a duplicate-object error mid-migration.Throws
Driver error such as "relation \"X\" already exists", "duplicate key value violates unique constraint __drizzle_migrations_pkey", or a lock timeout from the migrations tableRequired handlingCaller MUST either (a) ensure migrate() runs from exactly one process before traffic is sent to the new replicas (sidecar / initContainer / release-phase command), or (b) wrap migrate() in an application-level advisory lock (pg_advisory_lock() or an equivalent for the chosen driver) to serialize concurrent startups. Catching and retrying on duplicate-object errors is NOT safe — the colliding migration may have partially applied before the constraint violation, leaving the schema in a state neither process knows how to resume from.costhighin prodsilent failureusers seeservice unavailablevisibilitysilentSources[11] - invalidate · cache-invalidate-backend-errorerrorWhendb.$cache.invalidate({ tables: ... } | { tags: ... }) is called and the underlying Cache implementation throws — Redis connection lost, Upstash REST 401/429/500, Lua script timeout, invalid tag mapping in the composite-table set.Throws
Cache-backend-specific error (UpstashError / TypeError / generic Error from @upstash/redis) propagates as a Promise rejectionRequired handlingCaller MUST wrap db.$cache.invalidate() in try-catch. Invalidate is typically called immediately after a write that the next read must see — if the invalidate rejects and the caller does not handle it, the write succeeds but downstream reads continue to serve the stale cached value until TTL expiry. Recommended pattern: catch the cache error, log it with the table/tag that failed to invalidate, and either (a) extend the read-after-write window by busting the cache key directly through the backend client, or (b) accept the stale-read window and surface the metric to ops. NEVER swallow the error silently — silent invalidate failures are the dominant cause of "I updated the row but the API still returns the old value" bugs in cache-enabled drizzle apps.costmediumin prodsilent failureusers seedegraded performancevisibilitysilent - invalidate · cache-invalidate-silent-noop-when-cache-not-configuredwarningWhendb.$cache.invalidate(...) is called against a db that was constructed WITHOUT the cache: option in DrizzleConfig. invalidate resolves to undefined — no error, no warning, no log line.Throws
No error thrown — silently no-ops (NoopCache.onMutate returns immediately)Required handlingCalling code MUST NOT rely on invalidate() as a proof that downstream reads will reflect the latest write. Either (a) require the cache: option to be present (assert in app bootstrap and fail fast if missing), or (b) make read paths tolerant to stale values within a known TTL. In review, treat any code that calls $cache.invalidate as a tripwire — every caller MUST have a corresponding boot-time assertion that the cache is actually configured for the current environment. Otherwise the invalidate is dead code masquerading as correctness.costmediumin prodsilent failureusers seedegraded performancevisibilitysilentSources[14]
Sources
Every postcondition cites at least one of these. Grouped by source type; numbered to match the footnotes above.
Source code
- [6]raw.githubusercontent.com/drizzle-team/drizzle-orm/maindrizzle-team/drizzle-orm · errors.ts
- [9]raw.githubusercontent.com/drizzle-team/drizzle-orm/maindrizzle-team/drizzle-orm · query.ts
- [12]raw.githubusercontent.com/drizzle-team/drizzle-orm/maindrizzle-team/drizzle-orm · migrator.ts
- [13]raw.githubusercontent.com/drizzle-team/drizzle-orm/maindrizzle-team/drizzle-orm · dialect.ts
- [15]raw.githubusercontent.com/drizzle-team/drizzle-orm/maindrizzle-team/drizzle-orm · cache.ts
Other references
- [1]orm.drizzle.team/docs/selectSelect
- [2]orm.drizzle.team/docs/insertInsert
- [3]orm.drizzle.team/docs/updateUpdate
- [4]orm.drizzle.team/docs/deleteDelete
- [5]orm.drizzle.team/docs/transactionsTransactions
- [7]orm.drizzle.team/docs/sqlSql
- [8]orm.drizzle.team/docs/rqbRqb
- [10]orm.drizzle.team/docs/batch-apiBatch Api
- [11]orm.drizzle.team/docs/migrationsMigrations
- [14]orm.drizzle.team/docs/cacheCache
Research notes
Curator notes from SOURCES.md captured when the profile was written so you can verify the reasoning, not just the rules.
Sources: drizzle-orm
Package: drizzle-orm
Version: 0.45.1
Category: database (TypeScript ORM)
Official Documentation
- https://orm.drizzle.team/
- https://orm.drizzle.team/docs/select
- https://orm.drizzle.team/docs/insert
- https://github.com/drizzle-team/drizzle-orm
Behavioral Requirements
Database errors: Constraint violations, connection failures, SQL syntax errors Must wrap all CRUD operations in try-catch
Contract Rationale
TypeScript-first ORM - Type-safe queries still need runtime error handling Production-critical - Data integrity, constraint enforcement
Created: 2026-02-25 Status: ✅ COMPLETE
Need a different package?
Request a profile