Skip to content

Drivers Errors

GitHub Actions edited this page Sep 18, 2026 · 1 revision

Error Handling

Comprehensive error handling for all driver engines.

Deno Bun Node.js

Table of Contents

Overview

The drivers package provides a comprehensive error system with:

  • Standardized error codes across all database engines
  • Hierarchical error classes for type-safe error handling
  • Structured metadata with variable substitution
  • Cause chain preservation for debugging
  • Cross-runtime compatibility (Bun, Deno, Node.js)

All database-specific errors are mapped to standardized codes when a query runs through an engine's execute() / query methods, enabling consistent error handling regardless of the underlying database system. The one exception is the small set of escape-hatch transport clients — see Transport-Level Errors below.

Error Classes

DriverError

Base error class for the drivers package.

import { DriverError } from '@tundralibs/drivers/errors';

class DriverError<M extends Record<string, unknown>> extends BaseError<M> {
  constructor(message: string, meta: M, cause?: Error);
}

Properties:

  • message - Error message
  • context - Metadata object
  • cause - Optional underlying error
  • timeStamp - Error creation timestamp
  • stack - Stack trace

EngineError

Error thrown by every engine base class (ConnectionEngine, SQLConnectionEngine, SQLEngine, BaseEngine/PooledConnectionEngine) and the concrete engines built on them, for connection-lifecycle and engine-level failures.

import { EngineError } from '@tundralibs/drivers/errors';

class EngineError<M extends EngineErrorMeta> extends DriverError<M> {
  readonly code: EngineErrorCode;
  readonly engine: string;
  readonly connectionName: string;

  constructor(code: EngineErrorCode, meta: M, cause?: Error);
}

Properties:

  • code - Standardized error code (e.g., 'CONNECTION_FAILED')
  • engine - The engine's short identity code, e.g. 'POSTGRES', 'MARIA', 'REDIS' — not the class name. Set from each engine's own Engine field (PostgresEngine.Engine === 'POSTGRES'); a wire-compatible alias reports its own identity (CockroachEngine.Engine === 'COCKROACH'), not its parent's.
  • connectionName - Connection identifier
  • context - Error-specific metadata (e.g., { instanceId, timeoutMs })

Metadata:

All EngineError instances include:

  • instanceId - Formatted as "<Engine>::<Name>" (e.g. "POSTGRES::app-db")
  • Error-specific variables (e.g., operation, reason, timeoutMs)

EngineErrorCode

Union of the 30 standardized error-code strings accepted by the EngineError constructor and exposed on EngineError.code. Every one of them is listed in the Code Reference below.

import type { EngineErrorCode } from '@tundralibs/drivers/errors';

type EngineErrorCode =
  | 'UNKNOWN_ERROR'
  | 'INVALID_CONFIG_VALUE'
  | 'MISSING_CONFIG_VALUE'
  | 'CONNECTION_FAILED'
  | 'DISCONNECTION_FAILED'
  | 'NO_CONNECTION'
  | 'CONNECTION_LOST'
  | 'POOL_DRAINING'
  | 'POOL_ACQUIRE_TIMEOUT'
  | 'POOL_RESOURCE_FAILED'
  | 'OPERATION_FAILED'
  | 'UNSUPPORTED_OPERATION'
  | 'INVALID_AUTH'
  | 'PERMISSION_DENIED'
  | 'MISSING_PARAMETERS'
  | 'QUERY_EXECUTION_FAILED'
  | 'QUERY_TIMEOUT'
  | 'SYNTAX_ERROR'
  | 'DATABASE_NOT_FOUND'
  | 'TABLE_NOT_FOUND'
  | 'COLUMN_NOT_FOUND'
  | 'DUPLICATE_KEY'
  | 'FOREIGN_KEY_VIOLATION'
  | 'NOT_NULL_VIOLATION'
  | 'CHECK_VIOLATION'
  | 'DEADLOCK'
  | 'LOCK_TIMEOUT'
  | 'SERIALIZATION_FAILURE'
  | 'TRANSACTION_NOT_FOUND'
  | 'TRANSACTION_OPERATION_ERROR';

Each code is documented under Error Codes below.

EngineErrorMeta

Metadata shape carried by every EngineError. Each error code populates the variables its template requires (see per-code Metadata sections below).

import type { EngineErrorMeta } from '@tundralibs/drivers/errors';

EngineErrorCodes

Runtime constant mapping each EngineErrorCode to its message template. Templates use ${var} placeholders filled from error metadata when the message is built.

import { EngineErrorCodes } from '@tundralibs/drivers/errors';

// Look up the raw template for a code
EngineErrorCodes['CONNECTION_FAILED'];
// => 'Failed to connect to ${instanceId}'

Type: Record<EngineErrorCode, string>

Transport-Level Errors (Escape-Hatch Clients)

Four more DriverError subclasses exist for consumers who bypass the engine and drive its underlying transport directly. Each lives at its own engine's sub-path (not @tundralibs/drivers/errors) because it's transport-specific, not cross-engine:

Class Import Thrown by
PgServerError @tundralibs/drivers/postgres PgConnection, on a Postgres ErrorResponse message
NeonHttpError @tundralibs/drivers/neon NeonHttpClient.sql, on a non-2xx HTTP response
TursoHttpError @tundralibs/drivers/turso TursoHttpClient.execute, on a Hrana per-statement or pipeline error
D1HttpError @tundralibs/drivers/d1 D1HttpClient.query, on a non-2xx response or a success: false envelope

You will not see these through the normal engine API. PostgresEngine, NeonHttpEngine, TursoEngine, and D1Engine each catch their own transport error internally and re-throw the corresponding EngineError — that's what every example elsewhere in this doc catches. These four classes only reach your code if you construct and drive the transport class yourself, bypassing the engine (its connection-pool integration, OQL translation, and value coding included).

import { NeonHttpClient, NeonHttpError } from '@tundralibs/drivers/neon';

const client = new NeonHttpClient({
  host: 'ep-cool-name-a1b2c3.us-east-2.aws.neon.tech',
  connectionString:
    'postgresql://user:password@ep-cool-name-a1b2c3.us-east-2.aws.neon.tech/db',
});

try {
  await client.sql('SELECT * FROM users WHERE id = $1', [1]);
} catch (err) {
  if (err instanceof NeonHttpError) {
    // `code` is the Postgres SQLSTATE when Neon returned one; `status` is
    // the HTTP status of the failed response.
    console.error(
      `Neon query failed (${err.code ?? err.status}): ${err.message}`,
    );
  }
  throw err;
}

Error Codes

Code Reference

All 30 codes in EngineErrorCode, grouped the way EngineErrorCodes.ts groups them. Every EngineError carries instanceId ("<Engine>::<Name>"); the Metadata column lists the additional variables that code's message template consumes. Each code links to its detail section below.

Code Group Raised when Metadata
INVALID_CONFIG_VALUE Configuration An option failed validation — raised eagerly at construction, before any connection is attempted. option, reason
MISSING_CONFIG_VALUE Configuration A required option was not supplied. option
CONNECTION_FAILED Connection connect() could not establish a connection — bad host/port, network failure, or the server is down. —
DISCONNECTION_FAILED Connection disconnect() could not close cleanly. —
NO_CONNECTION Connection Internal safety net — a pooled connection was written to after it was already marked closed, or (Mongo only) the shared client was still null immediately after connect() resolved. Not the "you forgot to call connect()" error — see below. —
CONNECTION_LOST Connection An established connection dropped mid-flight — also mapped from Postgres 08xxx / 57Pxx SQLSTATEs. reason
POOL_DRAINING Pool A connection was requested while the pool is shutting down (during disconnect()). —
POOL_ACQUIRE_TIMEOUT Pool The wait for a free pooled connection exceeded acquireTimeoutSeconds. timeoutMs
POOL_RESOURCE_FAILED Pool Reserved for a failure to create a new pool resource. No throw site raises it today — the pool reports that as CONNECTION_FAILED. —
OPERATION_FAILED Operation Catch-all for a named engine operation that failed (cache get/set/delete, Mongo commands, HTTP round-trips). operation, reason
UNSUPPORTED_OPERATION Operation The engine has no implementation for the operation — e.g. transactions on the fetch-only HTTP engines. operation
INVALID_AUTH Auth Credentials were rejected — Postgres auth handshake, Redis AUTH, or SQLSTATE 28000 / 28P01. reason
PERMISSION_DENIED Auth Authenticated, but not authorized — SQLSTATE 42501, SQLite SQLITE_READONLY, Redis NOPERM, or Mongo Unauthorized. reason
MISSING_PARAMETERS Query The SQL names :param: placeholders that params does not supply. Raised before the query is sent. missing
QUERY_EXECUTION_FAILED Query The server rejected the statement for a reason with no more specific code. The default for unmapped SQLSTATEs. reason
QUERY_TIMEOUT Query The statement exceeded the engine's configured timeout — Postgres 57014, MariaDB ER_QUERY_TIMEOUT. timeoutMs
SYNTAX_ERROR Query The statement did not parse — SQLSTATE 42601, or a SQLite syntax error. reason
DATABASE_NOT_FOUND Schema The named database/catalog does not exist — SQLSTATE 3D000, MariaDB ER_BAD_DB_ERROR. database
TABLE_NOT_FOUND Schema The referenced table does not exist — SQLSTATE 42P01, or SQLite no such table. table
COLUMN_NOT_FOUND Schema The referenced column does not exist — SQLSTATE 42703, or SQLite no such column. column
DUPLICATE_KEY Constraint A UNIQUE or PRIMARY KEY constraint was violated — SQLSTATE 23505, MariaDB ER_DUP_ENTRY, SQLite SQLITE_CONSTRAINT_UNIQUE, Mongo DuplicateKey (11000). constraint
FOREIGN_KEY_VIOLATION Constraint A FOREIGN KEY constraint was violated — SQLSTATE 23503. constraint
NOT_NULL_VIOLATION Constraint A NOT NULL column received NULL — SQLSTATE 23502. column
CHECK_VIOLATION Constraint A CHECK constraint rejected the value — SQLSTATE 23514. constraint
DEADLOCK Concurrency The server broke a deadlock and chose this transaction as the victim — SQLSTATE 40P01. Retry it. —
LOCK_TIMEOUT Concurrency Waiting on a row/table lock timed out — SQLSTATE 55P03, MariaDB ER_LOCK_WAIT_TIMEOUT. —
SERIALIZATION_FAILURE Concurrency An MVCC conflict under SERIALIZABLE — SQLSTATE 40001. Retry it. —
TRANSACTION_NOT_FOUND Transaction A transactionId was passed that the engine does not know — usually already committed or rolled back. transactionId
TRANSACTION_OPERATION_ERROR Transaction begin / commit / rollback itself failed. operation, transactionId
UNKNOWN_ERROR Fallback The EngineError constructor received a code that is not in EngineErrorCodes. It coerces to this and preserves the original. reason, originalCode

Which codes you can actually see depends on the engine. The Postgres SQLSTATE map is shared verbatim with the Neon HTTP engine, so those two cover the widest range. SQLite's mapper — used by the native, Turso, and D1 engines — matches on the driver's error code and message text instead, and covers a narrower set: it never produces DEADLOCK, LOCK_TIMEOUT, QUERY_TIMEOUT, SERIALIZATION_FAILURE, DATABASE_NOT_FOUND, or INVALID_AUTH. Anything a mapper does not recognise becomes QUERY_EXECUTION_FAILED with the driver's own message on reason and the original error on cause.

Branching on a Code

err.code is the stable discriminator. Group the codes by the reaction they deserve rather than handling all 30 individually:

import { EngineError } from '@tundralibs/drivers/errors';
import type { EngineErrorCode } from '@tundralibs/drivers/errors';

/** Transient — the same call may succeed if you try it again. */
const RETRYABLE: ReadonlySet<EngineErrorCode> = new Set([
  'DEADLOCK',
  'SERIALIZATION_FAILURE',
  'LOCK_TIMEOUT',
  'POOL_ACQUIRE_TIMEOUT',
  'CONNECTION_LOST',
]);

/** Deployment/config problems — retrying will never help. */
const FATAL: ReadonlySet<EngineErrorCode> = new Set([
  'INVALID_CONFIG_VALUE',
  'MISSING_CONFIG_VALUE',
  'INVALID_AUTH',
  'PERMISSION_DENIED',
  'DATABASE_NOT_FOUND',
  'TABLE_NOT_FOUND',
  'COLUMN_NOT_FOUND',
  'SYNTAX_ERROR',
  'UNSUPPORTED_OPERATION',
]);

export type Verdict = 'retry' | 'fatal' | 'conflict' | 'other' | 'not-ours';

export function classify(err: unknown): Verdict {
  if (!(err instanceof EngineError)) return 'not-ours';
  if (RETRYABLE.has(err.code)) return 'retry';
  if (FATAL.has(err.code)) return 'fatal';
  switch (err.code) {
    case 'DUPLICATE_KEY':
    case 'FOREIGN_KEY_VIOLATION':
    case 'NOT_NULL_VIOLATION':
    case 'CHECK_VIOLATION':
      // A constraint spoke: this is data, not infrastructure. Surface it
      // to the caller (`err.context.constraint` names the constraint).
      return 'conflict';
    default:
      return 'other';
  }
}

Configuration Errors

INVALID_CONFIG_VALUE

Configuration value is invalid or malformed.

Template: Configuration value for "${option}" is invalid - ${reason}

Metadata:

  • option - Configuration option name
  • reason - Why the value is invalid

Example:

import { PostgresEngine } from '@tundralibs/drivers/postgres';
import { EngineError } from '@tundralibs/drivers/errors';

try {
  const engine = new PostgresEngine('db', {
    pool: { max: -5 }, // Invalid value
  });
} catch (err) {
  if (err instanceof EngineError && err.code === 'INVALID_CONFIG_VALUE') {
    console.error(`Invalid ${err.context.option}: ${err.context.reason}`);
  }
}

MISSING_CONFIG_VALUE

Required configuration value is not provided.

Template: Required configuration value "${option}" is missing

Metadata:

  • option - Missing configuration option name

Connection Lifecycle Errors

CONNECTION_FAILED

Failed to establish connection to the database.

Template: Failed to connect to ${instanceId}

Metadata:

  • instanceId - Engine instance identifier

Common Causes:

  • Invalid host/port
  • Network unreachable
  • Database not running
  • Authentication failure (may also throw INVALID_AUTH)

Example:

import { PostgresEngine } from '@tundralibs/drivers/postgres';
import { EngineError } from '@tundralibs/drivers/errors';

const engine = new PostgresEngine('db', { host: 'localhost', database: 'app' });

try {
  await engine.connect();
} catch (err) {
  if (err instanceof EngineError && err.code === 'CONNECTION_FAILED') {
    console.error(`Cannot connect to ${err.engine}::${err.connectionName}`);
    console.error('Cause:', (err.cause as Error | undefined)?.message);
  }
}

DISCONNECTION_FAILED

Failed to cleanly disconnect from the database.

Template: Failed to disconnect from ${instanceId}

Metadata:

  • instanceId - Engine instance identifier

NO_CONNECTION

An operation ran against a connection that had already been closed.

Template: No connection available for ${instanceId}

Metadata:

  • instanceId - Engine instance identifier

Not the "you forgot to call connect()" error. execute(), beginTransaction(), and every query method auto-connect on first use (if (this._status !== 'READY') await this.connect();) — calling them on a fresh, never-connected engine transparently connects instead of throwing. execute()'s own @throws list does not include NO_CONNECTION for exactly this reason. The code exists as an internal guard: PgConnection and RedisConnection throw it if a reserved connection is written to after it was already marked closed (a teardown racing an in-flight write), and MongoEngine throws it in the (deliberately hard to reach — the engine coalesces concurrent connect() calls into one in-flight attempt) case where its shared client is still null right after connect() resolved. None of these are triggerable from ordinary application code.

Example:

import { PostgresEngine } from '@tundralibs/drivers/postgres';

// No explicit connect() call — execute() connects lazily on first use.
const engine = new PostgresEngine('db', { host: 'localhost', database: 'app' });
const result = await engine.execute({ sql: 'SELECT 1 AS n' });
console.log(result.data[0]?.n); // 1
await engine.disconnect();

CONNECTION_LOST

Active connection was unexpectedly lost.

Template: Connection to ${instanceId} was lost: ${reason}

Metadata:

  • instanceId - Engine instance identifier
  • reason - Reason for connection loss

Pool Errors

POOL_DRAINING

Attempted to acquire connection while pool is draining.

Template: Pool for ${instanceId} is draining; new acquires are not permitted

Metadata:

  • instanceId - Engine instance identifier

Context:

Thrown when trying to acquire a connection during disconnect() or when the pool is shutting down.

POOL_ACQUIRE_TIMEOUT

Timed out waiting for available connection from pool.

Template: Acquiring a connection from ${instanceId} timed out after ${timeoutMs}ms

Metadata:

  • instanceId - Engine instance identifier
  • timeoutMs - Configured acquire timeout

Common Causes:

  • Pool exhausted (all connections in use)
  • Slow queries holding connections
  • Pool size too small for load

Example:

import { PostgresEngine } from '@tundralibs/drivers/postgres';
import { EngineError } from '@tundralibs/drivers/errors';

const engine = new PostgresEngine('db', {
  host: 'localhost',
  database: 'myapp',
  username: 'appuser',
  pool: {
    max: 5,
    acquireTimeoutSeconds: 30,
  },
});

try {
  // Running more concurrent queries than the pool's `max` makes later
  // acquires wait; past `acquireTimeoutSeconds` they reject.
  await Promise.all(
    Array.from(
      { length: 100 },
      () => engine.execute({ sql: 'SELECT pg_sleep(60)' }),
    ),
  );
} catch (err) {
  if (err instanceof EngineError && err.code === 'POOL_ACQUIRE_TIMEOUT') {
    console.error(`Pool exhausted after ${err.context.timeoutMs}ms`);
    // Consider increasing pool size or optimizing queries
  }
}

POOL_RESOURCE_FAILED

Reserved for a failure to create a new connection for the pool.

Template: Failed to create a new pool resource for ${instanceId}

Metadata:

  • instanceId - Engine instance identifier

Not currently thrown. No code path in the package raises this code. When the pool fails to create a resource for a queued waiter, it rejects that waiter with the underlying EngineError if there is one, and otherwise wraps the cause in a CONNECTION_FAILED. The code stays in the union for compatibility — do not write a handler that waits for it.

Operation Errors

OPERATION_FAILED

Generic operation failure.

Template: Operation "${operation}" failed on ${instanceId}: ${reason}

Metadata:

  • operation - Operation name
  • instanceId - Engine instance identifier
  • reason - Failure reason

Example:

import { MemcachedEngine } from '@tundralibs/drivers/memcached';
import { EngineError } from '@tundralibs/drivers/errors';

const cacheEngine = new MemcachedEngine('cache', { host: 'localhost' });

try {
  await cacheEngine.delete('key');
} catch (err) {
  if (err instanceof EngineError && err.code === 'OPERATION_FAILED') {
    console.error(`Failed ${err.context.operation}: ${err.context.reason}`);
  }
}

UNSUPPORTED_OPERATION

Operation is not supported by this engine.

Template: Operation "${operation}" is not supported by ${instanceId}

Metadata:

  • operation - Unsupported operation name
  • instanceId - Engine instance identifier

Authentication Errors

INVALID_AUTH

Authentication credentials are invalid.

Template: Authentication failed for ${instanceId}: ${reason}

Metadata:

  • instanceId - Engine instance identifier
  • reason - Authentication failure reason

Common Causes:

  • Wrong username/password
  • Expired credentials
  • Account disabled
  • Authentication method mismatch

Example:

import { PostgresEngine } from '@tundralibs/drivers/postgres';
import { EngineError } from '@tundralibs/drivers/errors';

const engine = new PostgresEngine('db', { host: 'localhost', database: 'app' });

try {
  await engine.connect();
} catch (err) {
  if (err instanceof EngineError && err.code === 'INVALID_AUTH') {
    console.error('Authentication failed - check credentials');
  }
}

PERMISSION_DENIED

User lacks required permissions for operation.

Template: Permission denied on ${instanceId}: ${reason}

Metadata:

  • instanceId - Engine instance identifier
  • reason - Permission denial reason

Query Errors

MISSING_PARAMETERS

Required query parameters not provided.

Template: Required parameters not provided for query on ${instanceId}: ${missing}

Metadata:

  • instanceId - Engine instance identifier
  • missing - Comma-separated list of missing parameter names

Example:

import { PostgresEngine } from '@tundralibs/drivers/postgres';
import { EngineError } from '@tundralibs/drivers/errors';

const engine = new PostgresEngine('db', { host: 'localhost', database: 'app' });

try {
  await engine.execute({
    sql: 'SELECT * FROM users WHERE id = :userId:',
    params: {}, // Missing userId parameter
  });
} catch (err) {
  if (err instanceof EngineError && err.code === 'MISSING_PARAMETERS') {
    console.error(`Missing params: ${err.context.missing}`);
  }
}

QUERY_EXECUTION_FAILED

Query execution encountered an error.

Template: Query execution failed on ${instanceId}: ${reason}

Metadata:

  • instanceId - Engine instance identifier
  • reason - Execution failure reason

Common Causes:

  • Runtime errors (division by zero, overflow)
  • Invalid function arguments
  • Data type mismatches
  • Constraint violations (see specific codes)

QUERY_TIMEOUT

Query exceeded configured timeout.

Template: Query timed out on ${instanceId} after ${timeoutMs}ms

Metadata:

  • instanceId - Engine instance identifier
  • timeoutMs - Configured query timeout

Example:

import { PostgresEngine } from '@tundralibs/drivers/postgres';
import { EngineError } from '@tundralibs/drivers/errors';

const engine = new PostgresEngine('db', { host: 'localhost', database: 'app' });

// A query timeout is configured on the engine (e.g. Postgres'
// `statementTimeoutMs`), not passed per call.
try {
  await engine.execute({ sql: 'SELECT * FROM huge_table' });
} catch (err) {
  if (err instanceof EngineError && err.code === 'QUERY_TIMEOUT') {
    console.warn(`Query exceeded ${err.context.timeoutMs}ms timeout`);
  }
}

SYNTAX_ERROR

SQL syntax is invalid.

Template: SQL syntax error on ${instanceId}: ${reason}

Metadata:

  • instanceId - Engine instance identifier
  • reason - Syntax error details

Schema Errors

DATABASE_NOT_FOUND

Referenced database does not exist.

Template: Database not found on ${instanceId}: ${database}

Metadata:

  • instanceId - Engine instance identifier
  • database - Database name

TABLE_NOT_FOUND

Referenced table does not exist.

Template: Table not found on ${instanceId}: ${table}

Metadata:

  • instanceId - Engine instance identifier
  • table - Table name

COLUMN_NOT_FOUND

Referenced column does not exist.

Template: Column not found on ${instanceId}: ${column}

Metadata:

  • instanceId - Engine instance identifier
  • column - Column name

Constraint Violations

DUPLICATE_KEY

Unique constraint or primary key violation.

Template: Duplicate key violation on ${instanceId}: ${constraint}

Metadata:

  • instanceId - Engine instance identifier
  • constraint - Constraint name

Example:

import { PostgresEngine } from '@tundralibs/drivers/postgres';
import { EngineError } from '@tundralibs/drivers/errors';

const engine = new PostgresEngine('db', { host: 'localhost', database: 'app' });

try {
  await engine.execute({
    sql: 'INSERT INTO users (email) VALUES (:email:)',
    params: { email: 'existing@example.com' },
  });
} catch (err) {
  if (err instanceof EngineError && err.code === 'DUPLICATE_KEY') {
    console.error(`Duplicate value for ${err.context.constraint}`);
    // Handle duplicate (e.g., return existing record)
  }
}

FOREIGN_KEY_VIOLATION

Foreign key constraint violation.

Template: Foreign key constraint violation on ${instanceId}: ${constraint}

Metadata:

  • instanceId - Engine instance identifier
  • constraint - Constraint name

NOT_NULL_VIOLATION

NOT NULL constraint violation.

Template: NOT NULL constraint violated on ${instanceId}: ${column}

Metadata:

  • instanceId - Engine instance identifier
  • column - Column name

CHECK_VIOLATION

CHECK constraint violation.

Template: CHECK constraint violated on ${instanceId}: ${constraint}

Metadata:

  • instanceId - Engine instance identifier
  • constraint - Constraint name

Concurrency Errors

DEADLOCK

Deadlock detected between concurrent transactions.

Template: Deadlock detected on ${instanceId}

Metadata:

  • instanceId - Engine instance identifier

Handling:

Deadlocks are typically transient - retry the transaction.

Example:

import { EngineError } from '@tundralibs/drivers/errors';

async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (err instanceof EngineError && err.code === 'DEADLOCK') {
        if (attempt < maxRetries - 1) {
          await new Promise((res) =>
            setTimeout(res, 100 * Math.pow(2, attempt))
          );
          continue;
        }
      }
      throw err;
    }
  }
  throw new Error('Unreachable');
}

LOCK_TIMEOUT

Timed out waiting for lock acquisition.

Template: Lock acquisition timed out on ${instanceId}

Metadata:

  • instanceId - Engine instance identifier

SERIALIZATION_FAILURE

Serialization failure due to concurrent update (MVCC).

Template: Serialization failure on ${instanceId} (concurrent update)

Metadata:

  • instanceId - Engine instance identifier

Context:

Common in SERIALIZABLE isolation level when concurrent transactions conflict.

Transaction Errors

TRANSACTION_NOT_FOUND

Referenced transaction does not exist.

Template: Transaction "${transactionId}" not found on ${instanceId}

Metadata:

  • instanceId - Engine instance identifier
  • transactionId - Transaction identifier

TRANSACTION_OPERATION_ERROR

Transaction operation failed.

Template: Transaction operation "${operation}" failed on ${instanceId} (txn ${transactionId})

Metadata:

  • operation - Transaction operation ('begin', 'commit', 'rollback')
  • instanceId - Engine instance identifier
  • transactionId - Transaction identifier

beginTransaction / commitTransaction / rollbackTransaction (used below) are the manual-handle primitives — the class JSDoc marks them @internal and says to prefer the callback form engine.transaction(fn) instead, which begins, commits on success, rolls back on any throw, and always releases the connection so it can never leak from the pool. Reach for the manual form only when you need to hold a transaction handle across a control flow transaction(fn) can't express (it's what norm's own executor uses). TRANSACTION_OPERATION_ERROR is raised by both forms — from a failed BEGIN (shown here), or from transaction(fn) if its callback swallows a statement error that already auto-rolled-back the transaction and the wrapper then finds nothing to commit. See transferFunds below for the callback form.

Example:

import { PostgresEngine } from '@tundralibs/drivers/postgres';
import { EngineError } from '@tundralibs/drivers/errors';

const engine = new PostgresEngine('db', { host: 'localhost', database: 'app' });

const txn = await engine.beginTransaction();
try {
  await engine.execute({ sql: 'INSERT INTO ...', transactionId: txn });
  await engine.commitTransaction(txn);
} catch (err) {
  await engine.rollbackTransaction(txn);
  if (
    err instanceof EngineError && err.code === 'TRANSACTION_OPERATION_ERROR'
  ) {
    console.error(`Transaction ${err.context.operation} failed`);
  }
  throw err;
}

Unknown Errors

UNKNOWN_ERROR

Fallback when an unrecognized error code is provided.

Template: Unknown error in ${instanceId}: ${reason}

Metadata:

  • instanceId - Engine instance identifier
  • reason - Error description
  • originalCode - The unrecognized code that was provided

Error Handling Patterns

Type-Safe Error Checking

import { EngineError } from '@tundralibs/drivers/errors';
import { PostgresEngine } from '@tundralibs/drivers/postgres';

const engine = new PostgresEngine('db', { host: 'localhost', database: 'app' });

try {
  await engine.execute({ sql: '...' });
} catch (err) {
  if (err instanceof EngineError) {
    // Type-safe access to error properties
    console.error(
      `Error ${err.code} from ${err.engine}::${err.connectionName}`,
    );
    console.error('Details:', err.context);

    // Handle specific error codes
    switch (err.code) {
      case 'DUPLICATE_KEY':
        // Handle duplicate
        break;
      case 'DEADLOCK':
        // Retry transaction
        break;
      case 'QUERY_TIMEOUT':
        // Log slow query
        break;
      default:
        throw err;
    }
  } else {
    // Unknown error type
    throw err;
  }
}

Error Recovery

A helper meant to work across engine types can't parameter-type on BaseEngine (or ConnectionEngine/SQLEngine): those base classes are generic over the concrete connection/options/events types, and TypeScript's structural check on their protected members means a concrete engine instance (PostgresEngine, MongoEngine, RedisEngine, …) is not assignable to the base type's own default type parameters — passing one is a compile error, not just a runtime mismatch. A minimal structural type that names only the method(s) you call sidesteps this and works for every engine.

import { EngineError } from '@tundralibs/drivers/errors';
import { PostgresEngine } from '@tundralibs/drivers/postgres';

async function connectWithRetry(
  engine: { connect(): Promise<void> },
  maxAttempts = 3,
  delayMs = 1000,
): Promise<void> {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      await engine.connect();
      return;
    } catch (err) {
      if (err instanceof EngineError && err.code === 'CONNECTION_FAILED') {
        if (attempt < maxAttempts) {
          console.warn(`Connection attempt ${attempt} failed, retrying...`);
          await new Promise((res) => setTimeout(res, delayMs * attempt));
          continue;
        }
      }
      throw err;
    }
  }
}

const engine = new PostgresEngine('db', { host: 'localhost', database: 'app' });
await connectWithRetry(engine);

Cause Chain Inspection

import { EngineError } from '@tundralibs/drivers/errors';

function inspectError(err: unknown): void {
  if (err instanceof EngineError) {
    console.error('Engine Error:', {
      code: err.code,
      engine: err.engine,
      connection: err.connectionName,
      message: err.message,
      metadata: err.context,
    });

    // Inspect cause chain
    let cause: unknown = err.cause;
    let depth = 1;
    while (cause) {
      console.error(`Cause ${depth}:`, (cause as Error).message);
      cause = (cause as { cause?: unknown }).cause;
      depth++;
    }
  }
}

Logging Structured Errors

import type { EngineError } from '@tundralibs/drivers/errors';

function logEngineError(err: EngineError): void {
  const logEntry = {
    timestamp: err.timeStamp.toISOString(),
    level: 'error',
    errorCode: err.code,
    engine: err.engine,
    connection: err.connectionName,
    message: err.message,
    metadata: err.context,
    stack: err.stack,
    cause: err.cause
      ? {
        message: (err.cause as Error).message,
        stack: (err.cause as Error).stack,
      }
      : undefined,
  };

  console.error(JSON.stringify(logEntry));
}

Examples

Complete Error Handling Flow

import { PostgresEngine } from '@tundralibs/drivers/postgres';
import { EngineError } from '@tundralibs/drivers/errors';

const db = new PostgresEngine('app-db', {
  host: 'localhost',
  port: 5432,
  database: 'myapp',
  username: 'appuser',
  password: 'secret',
  pool: {
    min: 2,
    max: 10,
    acquireTimeoutSeconds: 30,
  },
});

// Connection with retry
try {
  await db.connect();
} catch (err) {
  if (err instanceof EngineError) {
    if (err.code === 'CONNECTION_FAILED') {
      console.error('Cannot connect to database');
      console.error('Check host/port and database status');
    } else if (err.code === 'INVALID_AUTH') {
      console.error('Authentication failed - check credentials');
    }
    throw err;
  }
  throw err;
}

// Query with error handling
async function getUser(userId: number) {
  try {
    const result = await db.execute({
      sql: 'SELECT * FROM users WHERE id = :userId:',
      params: { userId },
    });
    return result.data[0];
  } catch (err) {
    if (err instanceof EngineError) {
      switch (err.code) {
        case 'TABLE_NOT_FOUND':
          console.error('Users table does not exist - run migrations');
          break;
        case 'QUERY_TIMEOUT':
          console.warn(`Query timed out after ${err.context.timeoutMs}ms`);
          break;
        case 'CONNECTION_LOST':
          // The pool discards the dead connection; the next execute()
          // acquires a fresh one.
          console.warn('Connection dropped mid-query, retrying once');
          return getUser(userId);
        default:
          console.error(`Query failed: ${err.code}`);
      }
    }
    throw err;
  }
}

// Transaction with error handling — engine.transaction(fn) begins, commits
// on success, rolls back on any throw, and always releases the connection.
// (beginTransaction / commitTransaction / rollbackTransaction, used above
// in the TRANSACTION_OPERATION_ERROR example, are the internal manual-handle
// primitives this wraps — prefer this callback form in application code.)
async function transferFunds(fromId: number, toId: number, amount: number) {
  try {
    await db.transaction(async (tx) => {
      await tx.execute({
        sql:
          'UPDATE accounts SET balance = balance - :amount: WHERE id = :fromId:',
        params: { amount, fromId },
      });

      await tx.execute({
        sql:
          'UPDATE accounts SET balance = balance + :amount: WHERE id = :toId:',
        params: { amount, toId },
      });
    });
  } catch (err) {
    // transaction() has already rolled back by the time this runs.
    if (err instanceof EngineError) {
      switch (err.code) {
        case 'DEADLOCK':
          console.warn('Deadlock detected - retry transaction');
          // Implement retry logic
          break;
        case 'SERIALIZATION_FAILURE':
          console.warn('Concurrent update conflict');
          // Implement retry logic
          break;
        case 'CHECK_VIOLATION':
          console.error('Insufficient funds or invalid amount');
          break;
        case 'TRANSACTION_OPERATION_ERROR':
          console.error(`Transaction ${err.context.operation} failed`);
          break;
        default:
          console.error(`Transaction failed: ${err.code}`);
      }
    }
    throw err;
  }
}

// Cleanup
await db.disconnect();

Multi-Engine Error Handling

import { PostgresEngine } from '@tundralibs/drivers/postgres';
import { RedisEngine } from '@tundralibs/drivers/redis';
import { EngineError } from '@tundralibs/drivers/errors';

async function initEngines() {
  const db = new PostgresEngine('db', {/* ... */});
  const cache = new RedisEngine('cache', {/* ... */});

  const engines = [db, cache];
  const errors: EngineError[] = [];

  // Connect all engines
  for (const engine of engines) {
    try {
      await engine.connect();
    } catch (err) {
      if (err instanceof EngineError) {
        errors.push(err);
        console.error(`Failed to connect ${err.engine}::${err.connectionName}`);
      }
    }
  }

  if (errors.length > 0) {
    throw new Error(`${errors.length} engine(s) failed to connect`);
  }

  return { db, cache };
}

← Back to Drivers

Clone this wiki locally