Typed Errors

Turbine throws typed errors you can catch with instanceof. Every error extends TurbineError and carries a stable readonly code field you can use for programmatic handling. Postgres driver errors (23505, 23503, 40P01, etc.) are translated into typed classes automatically via wrapPgError().

Error code table

CodeClassWhen thrown
TURBINE_E001NotFoundErrorfindUniqueOrThrow, findFirstOrThrow, update/delete against a non-matching row
TURBINE_E002TimeoutErrorQuery or transaction exceeds its configured timeout
TURBINE_E003ValidationErrorUnknown column, invalid operator, empty-where guard on update/delete
TURBINE_E004ConnectionErrorPool connection failure
TURBINE_E005RelationErrorUnknown relation name in a with clause
TURBINE_E006MigrationErrorMigration file parse error, checksum mismatch, apply failure
TURBINE_E007CircularRelationErrorRelation nesting depth exceeds 10
TURBINE_E008UniqueConstraintErrorpg 23505 — translated via wrapPgError()
TURBINE_E009ForeignKeyErrorpg 23503 — translated via wrapPgError()
TURBINE_E010NotNullViolationErrorpg 23502 — translated via wrapPgError()
TURBINE_E011CheckConstraintErrorpg 23514 — translated via wrapPgError()
TURBINE_E012DeadlockErrorpg 40P01 — retryable, exposes isRetryable: true
TURBINE_E013SerializationFailureErrorpg 40001 — retryable, exposes isRetryable: true
TURBINE_E014PipelineErrorNon-transactional pipeline has partial failures

Catching typed errors

Import the classes from the package root and narrow with instanceof.

import {
  NotFoundError,
  ValidationError,
  TimeoutError,
  UniqueConstraintError,
} from 'turbine-orm';
 
try {
  const user = await db.users.findUniqueOrThrow({ where: { id: 999 } });
} catch (err) {
  if (err instanceof NotFoundError) {
    // err.code === 'TURBINE_E001'
    // err.table, err.where, and err.operation are all populated
    return { status: 404 };
  }
  if (err instanceof ValidationError) {
    // err.code === 'TURBINE_E003'
    return { status: 400, message: err.message };
  }
  if (err instanceof TimeoutError) {
    // err.code === 'TURBINE_E002'
    // err.timeoutMs is the configured limit
    return { status: 504 };
  }
  throw err;
}

wrapPgError translation

Every query execution runs its pg error through wrapPgError(). The original driver error is preserved as .cause on the wrapped error.

pg SQLSTATENameTurbine class
23505unique_violationUniqueConstraintError
23503foreign_key_violationForeignKeyError
23502not_null_violationNotNullViolationError
23514check_violationCheckConstraintError
40P01deadlock_detectedDeadlockError (retryable)
40001serialization_failureSerializationFailureError (retryable)

Other pg errors pass through unchanged.

import { UniqueConstraintError } from 'turbine-orm';
 
try {
  await db.users.create({ data: { email: 'taken@example.com', ... } });
} catch (err) {
  if (err instanceof UniqueConstraintError) {
    // err.constraint — e.g. 'users_email_key'
    // err.columns   — e.g. ['email']
    // err.table     — e.g. 'users'
    // err.cause     — the original pg error
    return { status: 409, message: 'Email already in use' };
  }
  throw err;
}

Retryable errors

DeadlockError and SerializationFailureError expose a readonly isRetryable = true as const field. This isn't just a comment — it's a type-level signal so your retry loop narrows correctly.

import {
  DeadlockError,
  SerializationFailureError,
} from 'turbine-orm';
 
async function withRetry<T>(
  fn: () => Promise<T>,
  maxAttempts = 3,
): Promise<T> {
  let lastErr: unknown;
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      lastErr = err;
      if (
        (err instanceof DeadlockError && err.isRetryable) ||
        (err instanceof SerializationFailureError && err.isRetryable)
      ) {
        // Exponential backoff with jitter
        const backoff = 2 ** attempt * 50 + Math.random() * 25;
        await new Promise((r) => setTimeout(r, backoff));
        continue;
      }
      throw err;
    }
  }
  throw lastErr;
}
 
// Use it on a serializable transaction:
await withRetry(() =>
  db.$transaction(
    async (tx) => {
      const row = await tx.counters.findUniqueOrThrow({ where: { id: 1 } });
      await tx.counters.update({
        where: { id: 1 },
        data: { value: row.value + 1 },
      });
    },
    { isolationLevel: 'Serializable' },
  ),
);

Neither Prisma nor Drizzle surfaces a comparable typed retry signal — Prisma uses the stringly-typed code: 'P2034' on a generic PrismaClientKnownRequestError, and Drizzle bubbles up raw pg errors for you to grep SQL states from.

Pipeline errors

PipelineError is only thrown when a pipeline runs in non-transactional mode ({ transactional: false }) and one or more queries fail. It carries a per-query result array so you can inspect exactly which succeeded and which failed.

import { PipelineError } from 'turbine-orm';
 
try {
  await db.pipeline(queries, { transactional: false });
} catch (err) {
  if (err instanceof PipelineError) {
    // err.results: ({status:'ok', value} | {status:'error', error})[]
    // err.failedIndex: zero-based index of the first failed query
    // err.failedTag: DeferredQuery.tag of the first failure
    for (const [i, slot] of err.results.entries()) {
      if (slot.status === 'error') {
        console.error(`Query ${i} failed:`, slot.error);
      }
    }
  }
}

Transactional pipelines (the default) either fully succeed or roll back — a failure surfaces as whichever typed error the failing query raised.

Safe error messages

By default, NotFoundError messages include only the keys of the where clause — values are redacted so PII doesn't leak into logs (Sentry, Datadog, and friends).

[turbine] findUniqueOrThrow on "users" found no record matching where: { id, email }

The full where object is always available as err.where for programmatic access — only the human-readable message is redacted. Opt into verbose messages (useful in local development) by setting errorMessages: 'verbose' in your TurbineConfig, or by calling setErrorMessageMode('verbose') at startup.

const db = turbine({
  connectionString: process.env.DATABASE_URL,
  errorMessages: process.env.NODE_ENV === 'development' ? 'verbose' : 'safe',
});

See also