Profiles·Public

drizzle-orm

semver>=0.45.0 <1.0.0postconditions20functions11last verified2026-06-24coverage score73%

Postconditions: what we check

  • select · select-query-error
    error
    Whenquery fails (connection lost, timeout, invalid column, SQL syntax error)
    ThrowsError with database-specific error details
    Required 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 datavisibilityvisible
    Sources[1]
  • insert · insert-constraint-violation
    error
    Wheninsert violates constraint (unique, foreign key, not null, check constraint)
    ThrowsError with constraint violation details and database error code
    Required 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 datavisibilityvisible
    Sources[2]
  • update · update-constraint-violation
    error
    Whenupdate violates constraint or fails (unique, foreign key, connection lost)
    ThrowsError with constraint violation or connection error details
    Required 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 datavisibilityvisible
    Sources[3]
  • delete · delete-constraint-violation
    error
    Whendelete violates foreign key constraint (referenced by other tables)
    ThrowsError with foreign key constraint violation details
    Required 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 datavisibilityvisible
    Sources[4]
  • transaction · transaction-rollback-error
    error
    Whentx.rollback() is called inside the transaction callback
    ThrowsTransactionRollbackError (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
    Sources[5][6]
  • transaction · transaction-db-error-no-try-catch
    error
    Whendb.transaction() called without try-catch, and an operation inside throws
    ThrowsDatabase-specific error (e.g. PostgreSQL error code 23505, 23503, ECONNRESET) that propagates through transaction() after automatic rollback
    Required 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 unavailablevisibilityvisible
    Sources[5]
  • transaction · transaction-nested-savepoint
    warning
    Whendb.transaction() called inside another transaction (nested transaction via PgTransaction.transaction())
    ThrowsUses SAVEPOINT semantics — inner rollback only affects savepoint, not outer transaction; outer tx.rollback() affects everything
    Required 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 datavisibilitysilent
    Sources[5]
  • execute · execute-query-error
    error
    Whenraw SQL fails — syntax error, table not found, type mismatch, constraint violation
    ThrowsDrizzleQueryError with message 'Failed query: <sql>\nparams: <params>', wrapping the original driver error as .cause
    Required 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
    Sources[7][6]
  • execute · execute-type-mismatch-no-runtime-check
    warning
    Whensql<T> generic parameter does not match actual result shape at runtime
    ThrowsNo error thrown — TypeScript type is a lie; runtime data is wrong shape silently
    Required 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 datavisibilitysilent
    Sources[7]
  • findFirst · findfirst-undefined-on-missing-row
    error
    Whendb.query.<table>.findFirst({ where: ... }) is called and the filter matches zero rows. The promise resolves with `undefined`, not a thrown error.
    ThrowsNo error thrown — returns undefined when no row matches the filter
    Required 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
    Sources[8][9]
  • findFirst · findfirst-driver-error
    error
    Whendb.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.
    ThrowsDriver error (pg.DatabaseError / postgres.PostgresError / etc.) propagates as a Promise rejection
    Required 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 unavailablevisibilityvisible
    Sources[8]
  • findMany · findmany-driver-error
    error
    Whendb.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.
    ThrowsDriver error (pg.DatabaseError / postgres.PostgresError / etc.) propagates as a Promise rejection
    Required 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 unavailablevisibilityvisible
    Sources[8]
  • findMany · findmany-empty-array-on-stale-filter
    warning
    Whendb.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.
    ThrowsNo error thrown — returns [] silently when filter matches zero rows
    Required 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 performancevisibilitysilent
    Sources[8]
  • batch · batch-statement-failure-rolls-back-all
    error
    Whendb.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.
    ThrowsDriver-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 datavisibilityvisible
    Sources[10]
  • batch · batch-unsupported-on-non-batchable-driver
    warning
    Whendb.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.
    ThrowsTypeError: db.batch is not a function (runtime) — TypeScript catches it if the type is precise, but `any`-typed code reaches runtime
    Required 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 unavailablevisibilityvisible
    Sources[10]
  • migrate · migrate-missing-journal-or-sql-file
    error
    Whenmigrate(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.
    ThrowsError 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
    Sources[11][12]
  • migrate · migrate-driver-error-rolls-back-pending
    error
    Whenmigrate() 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.
    ThrowsDriver-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
    Sources[11][13]
  • migrate · migrate-no-built-in-lock-concurrent-startup-race
    warning
    Whenmigrate() 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.
    ThrowsDriver error such as "relation \"X\" already exists", "duplicate key value violates unique constraint __drizzle_migrations_pkey", or a lock timeout from the migrations table
    Required 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 unavailablevisibilitysilent
    Sources[11]
  • invalidate · cache-invalidate-backend-error
    error
    Whendb.$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.
    ThrowsCache-backend-specific error (UpstashError / TypeError / generic Error from @upstash/redis) propagates as a Promise rejection
    Required 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
    Sources[14][15]
  • invalidate · cache-invalidate-silent-noop-when-cache-not-configured
    warning
    Whendb.$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.
    ThrowsNo 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 performancevisibilitysilent
    Sources[14]

Sources

Every postcondition cites at least one of these. Grouped by source type; numbered to match the footnotes above.

Source code
Other references
  • [1]
    orm.drizzle.team/docs/select
    Select
  • [2]
    orm.drizzle.team/docs/insert
    Insert
  • [3]
    orm.drizzle.team/docs/update
    Update
  • [4]
    orm.drizzle.team/docs/delete
    Delete
  • [5]
    orm.drizzle.team/docs/transactions
    Transactions
  • [7]
    orm.drizzle.team/docs/sql
    Sql
  • [8]
    orm.drizzle.team/docs/rqb
    Rqb
  • [10]
    orm.drizzle.team/docs/batch-api
    Batch Api
  • [11]
    orm.drizzle.team/docs/migrations
    Migrations
  • [14]
    orm.drizzle.team/docs/cache
    Cache

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

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