PrormError, which itself extends the native Error. The typed subclasses let you branch on the kind of failure (a dropped connection, a violated constraint, a locked table) instead of pattern-matching raw driver message strings. This guide covers the class hierarchy in src/errors, which errors reach you from where, how to catch and tell them apart, the properties each one carries, and the utility helpers for classifying, retrying, and formatting them.[!IMPORTANT]
Most failures arrive already typed. Model validation throwsValidationError, transaction misuse throwsTransactionError, and the…OrFailfinders throwEmptyResultError. A statement the database
rejects for a unique, foreign-key or exclusion violation, a deadlock, a
statement timeout or a lock arrives asUniqueConstraintError,ForeignKeyConstraintError,ExclusionConstraintError,DeadlockError,TimeoutErrororResourceLockedError, with the driver error on.parent
and the statement on.sql. Anything else the database rejects (a syntax
error, a missing table) is re-thrown as the driver's own error; pass it
throughwrapError()at your
boundary if you want aDatabaseError. Where each error comes from
has the captured details. Before 4.9.2, constraint violations also arrived
as plain errors.
import {
PrormError,
DatabaseError,
ValidationError,
ConnectionError,
UniqueConstraintError,
ForeignKeyConstraintError,
EmptyResultError,
isPrormError,
isValidationError,
isRetryableError,
} from 'ts-prorm-orm';PrormError is the root. Most typed errors extend it directly; the constraint errors are the one place the tree goes deeper — UniqueConstraintError extends ValidationError (a unique violation is a kind of validation failure), while ForeignKeyConstraintError and ExclusionConstraintError extend PrormError directly.TransactionError lives in src/transaction-support.ts rather thansrc/errors, and several stores add their own DatabaseError subclassesDynamoDbError, DruidError, KustoError, the query-language…CapabilityErrors), so an instanceof DatabaseError check covers them too.PrormError, a single instanceof PrormError check separates prorm-originated failures from unexpected bugs in your own code.| Class | Extends | Default code | Notable properties |
|---|---|---|---|
PrormError | Error | UNKNOWN | code, parent, sql, metadata |
ConnectionError | PrormError | CONNECTION_ERROR | database, host |
DatabaseError | PrormError | DATABASE_ERROR (QUERY_ERROR from wrapError) | original |
ValidationError | PrormError | VALIDATION_ERROR | errors[] |
UniqueConstraintError | ValidationError | UNIQUE_CONSTRAINT_ERROR | fields[], errors[], parent, sql |
ForeignKeyConstraintError | PrormError | FOREIGN_KEY_CONSTRAINT_ERROR | field, table, value |
ExclusionConstraintError | PrormError | EXCLUSION_CONSTRAINT_ERROR | constraint |
ConnectionAcquireTimeoutError | ConnectionError | CONNECTION_TIMED_OUT | timeout (ms waited for a pooled connection) |
TimeoutError | PrormError | TIMEOUT_ERROR | timeout |
DeadlockError | PrormError | DEADLOCK_ERROR | parent, sql |
ResourceLockedError | PrormError | RESOURCE_LOCKED_ERROR | resource |
BulkRecordError | PrormError | BULK_RECORD_ERROR | errors[], errorCount |
AssociationError | PrormError | ASSOCIATION_ERROR | association |
EagerLoadError | PrormError | EAGER_LOAD_ERROR | model, association, associationType |
EmptyResultError | PrormError | UNKNOWN | model, primaryKey, where |
TransactionError | PrormError | UNKNOWN | dialect |
PrormError carries the same base set of fields, so any handler that catches the base type can rely on them:import { PrormError } from 'ts-prorm-orm';
try {
await user.save();
} catch (err: unknown) {
if (err instanceof PrormError) {
err.message; // human-readable summary
err.code; // stable ErrorCode string, e.g. 'UNIQUE_CONSTRAINT_ERROR'
err.parent; // the original driver Error, if this was wrapped
err.sql; // the SQL statement that triggered it, when available
err.metadata; // per-type extra data (fields, host, timeout, ...)
err.name; // the subclass name, e.g. 'UniqueConstraintError'
}
}toJSON() is defined on the base class, so any prorm error serializes cleanly for structured logging:console.log(JSON.stringify(err)); // { name, message, code, parent, sql, metadata, stack }owner VARCHAR(50) NOT NULL UNIQUElen rule) and an email column with an isEmail rule, running eachwrapError() made of it:| Call | What is thrown | After wrapError() |
|---|---|---|
Account.create({}) (a NOT NULL column left out) | ValidationError: owner cannot be null | unchanged |
Account.create({ owner: 'x', email: 'nope' }) | ValidationError: Validation len on owner failed, Validation isEmail on email failed | unchanged |
Account.create({ owner: 'ada' }) a second time | UniqueConstraintError: Query error: SqliteError: UNIQUE constraint failed: accounts.owner, fields: ['owner'], code UNIQUE_CONSTRAINT_ERROR | unchanged |
Transfer.create({ fromId: 99, … }) (no account 99) | ForeignKeyConstraintError: Query error: SqliteError: FOREIGN KEY constraint failed | unchanged |
prorm.query('SELEC 1') | the driver's SqliteError: near "SELEC": syntax error | DatabaseError, code QUERY_ERROR |
prorm.transaction(fn, { deferrable: true }) | TransactionError: sqlite has no DEFERRABLE transaction mode; drop `deferrable`. | unchanged |
Account.findByPkOrFail(42) | EmptyResultError: Instance with Account and primary key 42 not found | unchanged |
query() then classifies the failure by the driver's code (SQLSTATE,errno, SQL Server error number, ORA- code), falling back to its message,.parent and the statement on .sql:err.parent.message; // 'Query error: SqliteError: UNIQUE constraint failed: accounts.owner'
err.sql; // `INSERT INTO "accounts" ("owner") VALUES ('ada')`'error' event for these. Other driver errorsSELEC 1 row) are re-thrown unchanged.instanceof to branch on error type. Classify first (a driver error that is not a constraint, deadlock, timeout or lock arrives untyped, see above), then check specific subclasses before falling back to the base type:import {
PrormError,
ValidationError,
ConnectionError,
UniqueConstraintError,
wrapError,
} from 'ts-prorm-orm';
try {
await User.create({ email: 'taken@example.com' });
} catch (caught) {
const err =
caught instanceof PrormError || !(caught instanceof Error) ? caught : wrapError(caught);
if (err instanceof UniqueConstraintError) {
// a duplicate — most specific, so check it before ValidationError
console.error('Already taken:', err.fields);
} else if (err instanceof ValidationError) {
// any other validation failure
console.error('Invalid input:', err.getMessages());
} else if (err instanceof ConnectionError) {
console.error(`Cannot reach ${err.host}`);
} else if (err instanceof PrormError) {
console.error(`[${err.code}] ${err.message}`);
} else {
throw err; // not ours — re-throw
}
}UniqueConstraintError extends ValidationError, so a ValidationError check placed first would swallow it.import {
isPrormError,
isValidationError,
isConnectionError,
isConstraintError,
isEmptyResultError,
} from 'ts-prorm-orm';
if (isConstraintError(err)) {
// err is UniqueConstraintError | ForeignKeyConstraintError | ExclusionConstraintError
}
if (isEmptyResultError(err)) {
console.log(err.getMessage());
}code from the ErrorCodes map. Codes are useful when you want to route on a specific condition without importing the class — for example distinguishing a refused connection from a timed-out one, both of which are ConnectionError:import { ErrorCodes, isErrorCode } from 'ts-prorm-orm';
if (isErrorCode(err, ErrorCodes.CONNECTION_REFUSED)) {
// service is down
} else if (isErrorCode(err, ErrorCodes.CONNECTION_TIMED_OUT)) {
// slow — worth retrying
}ValidationError collects one or more ValidationErrorItem entries in its errors array. Each item records the message, type, and — when the failure is tied to a field — the path, value, and validator. Two helpers make the array easy to consume:import { ValidationError } from 'ts-prorm-orm';
try {
await user.validateRules();
} catch (err: unknown) {
if (err instanceof ValidationError) {
// Every message, flattened:
err.getMessages(); // ['Email is required', 'Age must be positive']
// Only the errors for one field:
for (const item of err.getFieldErrors('email')) {
console.log(item.message, item.value);
}
}
}UniqueConstraintError is a specialization of ValidationError, so it exposes the same errors array plus a fields list naming the columns that collided:if (err instanceof UniqueConstraintError) {
console.error(`Duplicate value for: ${err.fields?.join(', ')}`);
}prorm.query() already throw the typed class for constraintwrapError() is forPrormError it canDatabaseError. The.parent, and DatabaseError also keeps its.original.wrapError() and isRetryableError():| Driver message (abridged) | Class | code | Retryable |
|---|---|---|---|
duplicate key value violates unique constraint "users_email_key" | UniqueConstraintError | UNIQUE_CONSTRAINT_ERROR | no |
insert or update on table "orders" violates foreign key constraint "orders_userId_fkey" | ForeignKeyConstraintError | FOREIGN_KEY_CONSTRAINT_ERROR | no |
connect ECONNREFUSED 127.0.0.1:5432 | ConnectionError | CONNECTION_REFUSED | no |
canceling statement due to statement timeout | TimeoutError | TIMEOUT_ERROR | yes |
Lock wait timeout exceeded; try restarting transaction | TimeoutError | TIMEOUT_ERROR | yes |
could not serialize access due to concurrent update | DatabaseError | QUERY_ERROR | no |
Deadlock found when trying to get lock; try restarting transaction | DeadlockError | DEADLOCK_ERROR | yes |
database is locked | ResourceLockedError | RESOURCE_LOCKED_ERROR | yes |
[!NOTE]
Before 4.9.2, a message containing the wordtimeoutwas always aConnectionError(so a statement timeout read as a connection problem),
deadlocks were a non-retryableDatabaseError, andUniqueConstraintError
had codeVALIDATION_ERRORand dropped.parentand.sql. Code that
switched on those codes needs updating. A serialization failure (40001)
is still a non-retryableDatabaseError: retrying it safely means
re-running the whole transaction, which is whattransaction(fn, { retry: { max } })
does.
import { DatabaseError, ForeignKeyConstraintError, PrormError, wrapError } from 'ts-prorm-orm';
try {
await Order.create({ userId: 999 }); // no such user
} catch (caught) {
// Leave errors prorm already typed alone; classify the rest.
const err =
caught instanceof PrormError || !(caught instanceof Error) ? caught : wrapError(caught);
if (err instanceof ForeignKeyConstraintError) {
console.error(`Bad reference on ${err.table}.${err.field} = ${err.value}`);
} else if (err instanceof DatabaseError) {
console.error('Query failed:', err.original);
console.error('Offending SQL:', err.sql);
}
}import { wrapError } from 'ts-prorm-orm';
try {
await rawDriver.query(sql);
} catch (raw) {
throw wrapError(raw as Error, sql); // now a typed PrormError
}BulkRecordError aggregates per-record failures from a batch operation. Each entry pairs the failing record's index, the record itself, and its error:import { BulkRecordError } from 'ts-prorm-orm';
try {
await User.bulkCreate(rows);
} catch (err) {
if (err instanceof BulkRecordError) {
console.error(`${err.errorCount} of ${rows.length} records failed`);
for (const line of err.getMessages()) {
console.error(line); // "Record 3: Email already exists"
}
}
}ConnectionError records the database and host it was trying to reach, and its code distinguishes the failure mode. The class provides static factories that set the right code for you — these are what the internal connection handling uses:import { ConnectionError } from 'ts-prorm-orm';
throw ConnectionError.refused({ host: 'db.internal', database: 'app' });
throw ConnectionError.timedOut({ host: 'db.internal' });
throw ConnectionError.unreachable({ host: 'db.internal' });
throw ConnectionError.invalid({ database: 'app' });if (err instanceof ConnectionError) {
console.error(`Failed to reach ${err.host} (${err.code})`);
}findByPk / findOne style lookups that expect a row but find none raise EmptyResultError, which remembers what was searched for. Its getMessage() builds a descriptive sentence from whichever of model, primaryKey, and where are set:import { EmptyResultError } from 'ts-prorm-orm';
async function show(res: { status(code: number): { send(body: string): void } }) {
try {
return await User.findByPkOrFail(42);
} catch (err: unknown) {
if (err instanceof EmptyResultError) {
// "Instance with User and primary key 42 not found"
return res.status(404).send(err.getMessage());
}
throw err;
}
}isRetryableError returns true only for the transient classes — every TimeoutError, ResourceLockedError and DeadlockError, plus ConnectionErrors whose code is CONNECTION_TIMED_OUT or HOST_NOT_REACHABLE (which includes ConnectionAcquireTimeoutError). A deadlock victim's whole transaction was rolled back, so retry the transaction, not the single statement. Constraint and validation failures are deterministic and are never retryable.import { isRetryableError } from 'ts-prorm-orm';
async function withRetry<T>(op: () => Promise<T>, attempts = 3): Promise<T> {
for (let i = 0; ; i++) {
try {
return await op();
} catch (err) {
if (i >= attempts - 1 || !isRetryableError(err)) throw err;
await new Promise((r) => setTimeout(r, 100 * 2 ** i));
}
}
}
await withRetry(() => User.create({ email: 'a@b.com' }));getRootCause unwraps the .parent chain to the original driver error, and formatError renders any error — including the nested validation items — into a multi-line log string.import { getRootCause, formatError, wrapError, PrormError } from 'ts-prorm-orm';
try {
await user.save();
} catch (err) {
console.error(formatError(err instanceof Error && !(err instanceof PrormError) ? wrapError(err) : err));
// [UniqueConstraintError] Query error: SqliteError: UNIQUE constraint failed: accounts.owner
// Code: UNIQUE_CONSTRAINT_ERROR
// SQL: INSERT INTO "accounts" ("owner") VALUES ('ada')
// Parent: Query error: SqliteError: UNIQUE constraint failed: accounts.owner
// Errors:
// - unknown: Query error: SqliteError: UNIQUE constraint failed: accounts.owner
const root = err instanceof Error ? getRootCause(err) : err;
reportToSentry(root);
}SQL:Parent: lines appear when the error carries them: every typed error aprorm.query() throws does, and wrapError(err, sql) adds thegetRootCause() follows .parent to the driver's error.ValidationError from a model prints one line per attribute:[ValidationError] Validation len on owner failed, Validation isEmail on email failed
Code: VALIDATION_ERROR
Errors:
- owner: Validation len on owner failed
- email: Validation isEmail on email failedquery event on the Prorm instance, and one thaterror: true. That gives an error rate per model withoutprorm.on('query', (event: { sql: string; duration: number; error?: boolean }) => {
if (event.error) metrics.increment('db.query.errors');
});createValidationError makes a single-item error, and aggregateErrors merges several sources (existing ValidationErrors, item arrays, raw Errors, or strings) into one:import { createValidationError, aggregateErrors } from 'ts-prorm-orm';
const err = createValidationError('Age must be positive', 'age', -3);
const combined = aggregateErrors(
createValidationError('Email is required', 'email'),
'Name too short',
);
combined.getMessages(); // ['Email is required', 'Name too short']import {
PrormError,
ValidationError,
UniqueConstraintError,
ForeignKeyConstraintError,
EmptyResultError,
ConnectionError,
TimeoutError,
TransactionError,
wrapError,
isRetryableError,
formatError,
} from 'ts-prorm-orm';
type HttpError = { status: number; body: Record<string, unknown>; headers?: Record<string, string> };
export function toHttpError(caught: unknown): HttpError {
const err =
caught instanceof PrormError || !(caught instanceof Error) ? caught : wrapError(caught);
if (err instanceof UniqueConstraintError) {
return { status: 409, body: { error: 'conflict', fields: err.fields ?? [] } };
}
if (err instanceof ValidationError) {
const fields: Record<string, string[]> = {};
for (const item of err.errors) {
(fields[item.path ?? '_'] ??= []).push(item.message);
}
return { status: 422, body: { error: 'invalid', fields } };
}
if (err instanceof ForeignKeyConstraintError) {
return { status: 422, body: { error: 'unknown_reference', field: err.field } };
}
if (err instanceof EmptyResultError) {
return { status: 404, body: { error: 'not_found' } };
}
if (isRetryableError(err) || err instanceof ConnectionError || err instanceof TimeoutError) {
return { status: 503, body: { error: 'unavailable' }, headers: { 'Retry-After': '2' } };
}
if (err instanceof TransactionError) {
// A programming error: an option the engine cannot honour. Log loudly.
console.error(formatError(err));
}
return { status: 500, body: { error: 'internal' } };
}UniqueConstraintError is checked before ValidationError because it is one;wrapError() first: driver rejections arrive as plain errors. Then narrow to specific subclasses.UniqueConstraintError before ValidationError.code for a stable, class-independent switch; read parent / sql / original for the underlying driver detail.isRetryableError to decide what to retry, and formatError / getRootCause when logging.