A first-response order for "the database layer is broken". The point is to
classify quickly, because the four categories below have nothing to do with
each other.Classify the error#
Every error prorm raises extends PrormError, and the type guards tell you
which family you are in without string-matching a message:import {
isConnectionError, isConstraintError, isValidationError, isRetryableError,
} from 'ts-prorm-orm';
if (isConnectionError(err)) { /* can't reach the database */ }
if (isConstraintError(err)) { /* reached it; it rejected the row */ }
if (isValidationError(err)) { /* never reached it */ }
if (isRetryableError(err)) { /* transient; safe to try again */ }
That single distinction decides everything that follows:The order of the questions is the order of the type guards in
src/errors/utils.ts; each answer points at one runbook or one part of the
application, never at more than one.| Family | Meaning | Where to look |
|---|
ConnectionError | Never reached the database | Connection failures |
TimeoutError | Reached it; it did not answer in time | Slow query, or lock contention |
UniqueConstraintError etc. | Reached it; it rejected the write | Application logic, or a bad backfill |
ValidationError | Never left the process | Application logic |
Is it everything, or one query?#
await db.authenticate(); // round-trips a trivial query
If that succeeds, the database is reachable and the problem is a specific
query, table or lock — not connectivity. If it fails, go to
Connection failures.Is the pool exhausted?#
A pool with no free connection produces timeouts that look like a slow
database. Distinguish them: if queries are queuing but the database itself is
idle, the pool is the bottleneck, not the engine.const db = new Prorm({
/* … */
pool: { max: 20, acquire: 30000 },
});
max and acquire reach the driver's pool. enableEvents and
logPoolOperations do not: they report on a bookkeeping pool that queries
never check a connection out of, so they stay silent through a real leak, and
getPoolStats() describes the same bookkeeping pool.Look for a leak from the database side instead. A leaked connection is usually
a transaction that threw without a finally, and it shows as a session held
open by your application doing nothing:On MySQL and MariaDB, SHOW PROCESSLIST lists the same thing as Sleep
connections with a long Time, and information_schema.innodb_trx shows which
of them still hold a transaction open.Turn on the SQL#
const db = new Prorm({ /* … */ logging: (sql, ms) => console.log(ms, sql) });
Or per query, so you do not drown in output:await User.findAll({ where: { id }, logging: console.log });
benchmark: true adds timings, logSlowQueries with slowQueryThreshold
reports only the queries that exceed it.Decide the remedy#
Transient and isRetryableError → let the retry policy handle it; consider
raising retry.max.
Is it a security incident?#
[!CAUTION]
Rows nobody wrote, a grant nobody made, a user nobody created, or a burst
of failed logins is not an outage; treat it as a leak until shown otherwise
and go to If a credential leaked.
Revoke first, restore service second.
Two tools make the question answerable rather than a guess:$ prorm audit db --url "$DATABASE_URL" # DB-PRIV, DB-GRANTS, DB-READONLY: who can do what, right now
$ prorm audit code . # SEC-001 / SEC-003: a secret in the code or a committed .env
$ prorm logs escalations --unacked --since 24h # open incidents from the escalation policy
$ prorm logs tail --level critical+ --since 24h # the audit entries behind them
If the application runs an escalation policy,
the security-incident, security-privilege-change and
security-mass-deletion presets are the ones that open incidents for exactly
these signals; prorm logs presets --keyword security lists them. Modified at 2026-10-04 00:40:16