Skip to content

Errors ​

kick/db reports problems as classes you can instanceof, each carrying what you need to act on it. Query failures come from the database; the rest come from kick/db itself — a relational query that can't be built, a migration the runner refuses.

Every error extends KickDbError and has a stable code.

Query errors ​

A failed query throws one of these instead of the driver's own error — on Postgres, MySQL and SQLite, for queries, transactions (including a serializable COMMIT), savepoints and connecting:

ErrorWhenExtras
UniqueViolationErrora row duplicates a unique or primary keystatus: 409
ForeignKeyViolationErrora row references a missing row, or a referenced row is deleted
CheckViolationErrora CHECK constraint rejects the row
NotNullViolationErrora NOT NULL column gets null
SerializationFailureErrora transaction conflicted with a concurrent oneretryable: true
DeadlockErrorthe database cancelled one side of a deadlockretryable: true
ConnectionErrorthe database can't be reached, or dropped the connection
DatabaseErroranything else the database reports — the base class

Each carries what the database said:

FieldMeaning
dialect'postgres', 'mysql' or 'sqlite'
driverCodeSQLSTATE (Postgres, MySQL) or the driver's code (SQLITE_CONSTRAINT_UNIQUE, ER_DUP_ENTRY)
constraintthe constraint's name
tablethe table
columnsthe columns involved, in key order
detailthe database's own explanation (Postgres)
causethe driver's original error

Not every database says everything:

ErrorPostgresMySQLSQLite
Uniqueconstraint, table, columns, detailconstraint, tabletable, columns
Foreign keyconstraint, table, columns, detailconstraint, table, columns—
Checkconstraint, tableconstraintconstraint
Not nulltable, columnscolumnstable, columns

Errors that don't come from the database — a TypeError in your own code — pass through untouched.

Handling them ​

Catch the kind you can do something about and rethrow the rest:

ts
import { HttpException } from '@forinda/kickjs'
import { ForeignKeyViolationError, UniqueViolationError } from '@forinda/kickjs-db'

try {
  await this.db.insertInto('members').values({ teamId, email }).execute()
} catch (err) {
  if (err instanceof UniqueViolationError && err.columns.includes('email')) {
    throw HttpException.conflict('That email is already on the team')
  }
  if (err instanceof ForeignKeyViolationError) {
    throw HttpException.notFound('No such team')
  }
  throw err
}

Left unhandled, a UniqueViolationError answers 409 rather than 500 — it carries status: 409. Its message names the table and columns, never the duplicate value. Other database errors answer 500 and are logged.

Retryable errors — serialization failures and deadlocks — mean "run the transaction again". Let the transaction do it with retry rather than catching them yourself.

Calling a driver directly? translateDbError(err, dialect) turns its error into one of these classes:

ts
import { translateDbError } from '@forinda/kickjs-db'

try {
  await pool.query('…')
} catch (err) {
  throw translateDbError(err, 'postgres')
}

Error reference ​

Query ​

Error / codeCauseFix
UniqueViolationError · unique_violationduplicate value for a unique / primary keycheck first, or catch it and answer 409; for "insert or update" use an upsert (recipes)
ForeignKeyViolationError · foreign_key_violationparent row missing, or still referenced on deletevalidate the id first; on delete, use onDelete: 'cascade' / 'set_null' or delete children first
CheckViolationError · check_violationa check() constraint failedvalidate input against the same rule (err.constraint names it)
NotNullViolationError · not_null_violationnull for a NOT NULL columngive the column a value or a .default()
SerializationFailureError · serialization_failureconcurrent transactions conflicted (Postgres 40001, MySQL lock wait timeout, SQLite busy)transaction({ retry: true })
DeadlockError · deadlocktwo transactions waited on each othertransaction({ retry: true }); touch rows in a consistent order
ConnectionError · connection_errordatabase unreachable or connection droppedcheck the connection settings and that the database is up; pool limits
DatabaseError · database_erroranything else (driverCode says what)read err.message / err.cause
TransactionFinishedError · transaction_finisheda query ran after its transaction had committed or rolled back — usually a promise inside transaction() that wasn't awaitedawait every query inside transaction(); move work meant for after the commit into afterCommit()

Relational queries (db.query.*) ​

Error / codeCauseFix
RelationalQueryUnknownRelationError · KICK_DB_RELATIONAL_UNKNOWN_RELATIONa with key that isn't declareddeclare it in relations() (Relational Queries)
RelationalQueryDepthError · KICK_DB_RELATIONAL_DEPTH_EXCEEDEDwith nested deeper than maxDepth (default 5)nest less, or pass { maxDepth: N } if it's intended
RelationalQueryAliasCollisionError · KICK_DB_RELATIONAL_ALIAS_COLLISIONa relation has the same name as a columnrename the relation or the column
RelationalQueryMissingInverseError · KICK_DB_RELATIONAL_MISSING_INVERSEa many with no one pointing back, and zero or several foreign keys to pairdeclare the inverse one, or tag both sides with the same relationName
RelationalQueryThroughError · KICK_DB_RELATIONAL_THROUGHa many(…, { through }) junction doesn't have exactly one foreign key to each side, or from / to aren't onename the junction columns with { table, from, to }, or fix its references()
RelationalQueryAmbiguousRelationNameError · KICK_DB_RELATIONAL_AMBIGUOUS_RELATION_NAMEseveral one relations share a relationNamemake each relationName unique per pair of tables
RelationalQueryCancelledError · relational_query_cancelledthe signal passed to the query abortedexpected when a request is cancelled; err.cause holds the abort reason
KICK_DB_RELATIONAL_NOT_SUPPORTED (a KickDbError)the MySQL migration adapter found MySQL older than 8.0 or MariaDB older than 10.5 — relational queries need JSON_ARRAYAGGupgrade the server, or use the query builder instead of db.query
KICK_DB_POOL_NOT_CLOSABLE (a KickDbError)pgAdapter / mysqlAdapter got endPoolOnClose: true with a pool that has no end()pass a pool with end(), or leave endPoolOnClose off and close the pool yourself

Migrations ​

Error / codeCauseFix
UnreviewedMigrationError · migration_unrevieweda migration with reviewed: false applied outside developmentread its SQL, then kick db migrate review <id>; in tests, requireReviewed: false
MigrationHashError · migration_hash_mismatcha reviewed migration's files changed after review (expected / actual)undo the edit; or, if it hasn't run anywhere, review it again. Applied migrations are immutable — put the change in a new one
MigrationFailedError · migration_faileda migration's SQL failed; the message names it, the driver error is cause; it stays pendingfix the SQL (review it again), or fix the data it tripped on, then rerun
MigrationDriftError · migration_driftthe live database differs from the last applied snapshot (named in the message; err.diff: added / removed / changed)find who changed the database by hand; fold the change into a migration, or revert it
MigrationLockError · migration_lock_heldanother migration run holds the lockwait for it; if a crashed run left it, clear it (below)
MigrationEnumDropError · migration_enum_drop_unconfirmedthe migration removes Postgres enum valuesreview the USING casts in up.sql, then --confirm-enum-drop / confirmEnumDrop: true
RemovedValueAsDefaultError · removed_value_as_defaultkick db generate: an enum value being removed is a column's defaultchange or drop that default first
CompositeEnumReferenceError · composite_enum_referencean enum value removal blocked by a composite type using the enumchange the composite type by hand, then generate again
SqliteRebuildRequiredErroremitSqlite needed a table rebuild but wasn't given both snapshotspass { from, to } — kick db generate always does

A run that crashed mid-migration can leave the lock held. Check locked_by (process id and start time) to be sure nothing is running, then:

sql
UPDATE kick_migrations_lock SET locked_at = NULL, locked_by = NULL WHERE id = 1;

Released under the MIT License. Built with TypeScript — runs on Express, Fastify, or h3.