/** * @module @dotdo/postgres-shared/errors * * Standardized error handling for all postgres packages. * Provides a consistent error hierarchy with: * - Error codes for programmatic handling * - Original error cause preservation * - Context information for debugging * - Timestamp for timing information * - Serialization for Workers * - Recovery patterns (retryable, suggestions, circuit breaker categories) */ import { sanitizeConnectionString, sanitizeErrorMessage, } from './sanitize.js' // ============================================================================= // Error Codes // ============================================================================= /** * Standard error codes for categorizing errors */ export type PostgresErrorCode = // General errors | 'UNKNOWN' | 'INTERNAL' | 'NOT_IMPLEMENTED' // Query errors | 'QUERY_ERROR' | 'QUERY_SYNTAX' | 'QUERY_TIMEOUT' | 'QUERY_CANCELLED' | 'TABLE_NOT_FOUND' | 'COLUMN_NOT_FOUND' | 'CONSTRAINT_VIOLATION' | 'UNIQUE_VIOLATION' | 'FOREIGN_KEY_VIOLATION' | 'NULL_VIOLATION' | 'CHECK_VIOLATION' // Connection errors | 'CONNECTION_ERROR' | 'CONNECTION_TIMEOUT' | 'CONNECTION_REFUSED' | 'CONNECTION_RESET' | 'CONNECTION_CLOSED' | 'NETWORK_ERROR' | 'DNS_ERROR' // Validation errors | 'VALIDATION_ERROR' | 'INVALID_PARAMETER' | 'INVALID_CONFIG' | 'INVALID_TYPE' | 'INVALID_FORMAT' // Authentication errors | 'AUTH_ERROR' | 'AUTH_FAILED' | 'AUTH_EXPIRED' | 'AUTH_REQUIRED' | 'AUTH_FORBIDDEN' // Protocol errors | 'PROTOCOL_ERROR' | 'PROTOCOL_VERSION' | 'MESSAGE_TOO_LARGE' | 'INVALID_MESSAGE' // State errors | 'INVALID_STATE' | 'CONFLICT' | 'CONCURRENCY_ERROR' | 'LOCK_ERROR' // Resource errors | 'NOT_FOUND' | 'ALREADY_EXISTS' | 'RATE_LIMITED' | 'QUOTA_EXCEEDED' | 'RESOURCE_EXHAUSTED' // ============================================================================= // Error Context // ============================================================================= /** * Context information for debugging errors */ export interface ErrorContext { /** SQL query that caused the error */ sql?: string /** Query parameters */ params?: unknown[] /** Table name involved */ table?: string /** Column name involved */ column?: string /** Schema name involved */ schema?: string /** Constraint name involved */ constraint?: string /** Database name */ database?: string /** Connection URL (sanitized) */ connection?: string /** Additional arbitrary context */ [key: string]: unknown } // ============================================================================= // Error Types for Standardization // ============================================================================= /** Error category for circuit breaker decisions */ export type ErrorCategory = 'TRANSIENT' | 'PERMANENT' | 'UNKNOWN' /** Retry configuration for calculateRetryDelay */ export interface RetryConfig { baseDelayMs: number maxDelayMs: number jitter?: boolean } /** Metrics format for observability */ export interface ErrorMetrics { error_name: string error_code: string status_code: number retryable: boolean labels: { error_type: string error_code: string } } /** Worker response format for safe serialization */ export interface WorkerErrorResponse { __errorType: string code: PostgresErrorCode message: string statusCode: number retryable: boolean context: ErrorContext } /** Standard JSON format for error serialization */ export interface StandardErrorJSON { name: string code: string message: string statusCode: number retryable: boolean context: Record timestamp: string stack?: string cause?: unknown suggestion?: string } // ============================================================================= // Base PostgresError // ============================================================================= /** * Base error class for all postgres-related errors. * * All custom errors in the postgres ecosystem should extend this class * to ensure consistent error handling patterns. * * @example * ```typescript * try { * await db.query('SELECT * FROM users') * } catch (error) { * if (error instanceof PostgresError) { * console.log('Error code:', error.code) * console.log('Context:', error.context) * console.log('Cause:', error.cause) * } * } * ``` */ export class PostgresError extends Error { /** Error code for programmatic handling */ readonly code: PostgresErrorCode /** Context information for debugging */ readonly context: ErrorContext /** HTTP status code equivalent (for API responses) */ readonly statusCode: number /** Whether this error can be retried */ readonly retryable: boolean /** Timestamp when the error was created */ readonly timestamp: Date /** Recovery suggestion for this error */ readonly suggestion?: string constructor( message: string, options: { code?: PostgresErrorCode cause?: Error | unknown context?: ErrorContext statusCode?: number retryable?: boolean suggestion?: string } = {} ) { super(message, { cause: options.cause }) this.name = 'PostgresError' this.code = options.code ?? 'UNKNOWN' this.context = options.context ?? {} this.statusCode = options.statusCode ?? 500 this.retryable = options.retryable ?? false this.timestamp = new Date() if (options.suggestion !== undefined) { this.suggestion = options.suggestion } // Maintain proper prototype chain Object.setPrototypeOf(this, new.target.prototype) } /** * Convert to a plain object for JSON serialization */ toJSON(): StandardErrorJSON { const result: StandardErrorJSON = { name: this.name, code: this.code, message: this.message, context: this.context, statusCode: this.statusCode, retryable: this.retryable, timestamp: this.timestamp.toISOString(), } if (this.stack !== undefined) { result.stack = this.stack } if (this.cause !== undefined) { result.cause = this.cause instanceof Error ? { name: this.cause.name, message: this.cause.message } : this.cause } if (this.suggestion) { result.suggestion = this.suggestion } return result } /** * Create an error response object for API responses */ toResponse(): { error: true code: PostgresErrorCode message: string details?: ErrorContext } { const hasDetails = Object.keys(this.context).length > 0 if (hasDetails) { return { error: true, code: this.code, message: this.message, details: this.context, } } return { error: true, code: this.code, message: this.message, } } /** * Create a serializable error format for Worker responses. * Includes a type discriminator for reconstruction. */ toWorkerResponse(): WorkerErrorResponse { return { __errorType: this.name, code: this.code, message: this.message, statusCode: this.statusCode, retryable: this.retryable, context: this.context, } } /** * Get the root cause of the error chain. */ getRootCause(): Error { let current: Error = this while (current.cause instanceof Error) { current = current.cause } return current } /** * Calculate retry delay with exponential backoff. * For RateLimitError, respects retryAfterMs if present. */ calculateRetryDelay(attempt: number, config: RetryConfig): number { // Check if this is a RateLimitError with retryAfterMs if ('retryAfterMs' in this && typeof (this as unknown as { retryAfterMs: number }).retryAfterMs === 'number') { const retryAfterMs = (this as unknown as { retryAfterMs: number }).retryAfterMs if (retryAfterMs > 0) { return retryAfterMs } } // Calculate exponential backoff: baseDelay * 2^(attempt-1) const exponentialDelay = config.baseDelayMs * Math.pow(2, attempt - 1) const cappedDelay = Math.min(exponentialDelay, config.maxDelayMs) // Add jitter if requested (0-100% of delay) if (config.jitter) { const jitterFactor = Math.random() return cappedDelay + cappedDelay * jitterFactor } return cappedDelay } /** * Get the circuit breaker category for this error. * - TRANSIENT: temporary failures (connection issues, timeouts) - retry may help * - PERMANENT: won't succeed on retry (validation, not found) * - UNKNOWN: unclear if retry will help */ getCircuitBreakerCategory(): ErrorCategory { // Connection-related errors are transient const transientCodes: PostgresErrorCode[] = [ 'CONNECTION_ERROR', 'CONNECTION_TIMEOUT', 'CONNECTION_REFUSED', 'CONNECTION_RESET', 'CONNECTION_CLOSED', 'NETWORK_ERROR', 'DNS_ERROR', 'RATE_LIMITED', 'RESOURCE_EXHAUSTED', ] // Validation and query syntax errors are permanent const permanentCodes: PostgresErrorCode[] = [ 'VALIDATION_ERROR', 'INVALID_PARAMETER', 'INVALID_CONFIG', 'INVALID_TYPE', 'INVALID_FORMAT', 'QUERY_SYNTAX', 'NOT_FOUND', 'AUTH_FORBIDDEN', 'CONSTRAINT_VIOLATION', 'UNIQUE_VIOLATION', 'FOREIGN_KEY_VIOLATION', 'NULL_VIOLATION', 'CHECK_VIOLATION', 'TABLE_NOT_FOUND', 'COLUMN_NOT_FOUND', ] if (transientCodes.includes(this.code)) { return 'TRANSIENT' } if (permanentCodes.includes(this.code)) { return 'PERMANENT' } return 'UNKNOWN' } /** * Convert error to metrics format for observability. */ toMetrics(): ErrorMetrics { return { error_name: this.name, error_code: this.code, status_code: this.statusCode, retryable: this.retryable, labels: { error_type: this.name, error_code: this.code, }, } } /** * Reconstruct a PostgresError from JSON serialized data. * Used for deserializing errors that crossed Worker boundaries. */ static fromJSON(json: StandardErrorJSON): PostgresError { // Reconstruct cause if present let cause: Error | undefined if (json.cause && typeof json.cause === 'object' && 'message' in json.cause) { const causeData = json.cause as { name?: string; message: string } cause = new Error(causeData.message) if (causeData.name) { cause.name = causeData.name } } // Create base error with options const options: { code: PostgresErrorCode context: ErrorContext statusCode: number retryable: boolean cause?: Error suggestion?: string } = { code: json.code as PostgresErrorCode, context: json.context as ErrorContext, statusCode: json.statusCode, retryable: json.retryable, } if (cause) options.cause = cause if (json.suggestion) options.suggestion = json.suggestion // Determine the error class based on name and create appropriate instance switch (json.name) { // Query errors case 'QueryError': return new QueryError(json.message, options) case 'QueryTimeoutError': { const timeoutMs = (json.context as { timeoutMs?: number })?.timeoutMs ?? 0 return new QueryTimeoutError(timeoutMs, options) } case 'ConstraintError': return new ConstraintError(json.message, options) case 'WriteBlockedError': { const sql = (json.context as { sql?: string })?.sql ?? '' return new WriteBlockedError(sql) } // Connection errors case 'ConnectionError': return new ConnectionError(json.message, options) case 'ConnectionTimeoutError': { const timeoutMs = (json.context as { timeoutMs?: number })?.timeoutMs ?? 0 return new ConnectionTimeoutError(timeoutMs, options) } case 'PoolError': return new PoolError(json.message, options) // Validation errors case 'ValidationError': return new ValidationError(json.message, options) case 'ConfigError': return new ConfigError(json.message, options) case 'InvalidIdentifierError': { const ctx = json.context as { identifier?: string; identifierType?: string; reason?: string } return new InvalidIdentifierError( ctx.identifier ?? '', ctx.identifierType ?? 'unknown', ctx.reason ?? '' ) } // Authentication errors case 'AuthenticationError': return new AuthenticationError(json.message, options) // Protocol errors case 'ProtocolError': return new ProtocolError(json.message, options) // State errors case 'StateError': { const ctx = json.context as { currentState?: string; expectedStates?: string[] } return new StateError(json.message, { currentState: ctx.currentState ?? 'unknown', expectedStates: ctx.expectedStates ?? [], cause, context: json.context as ErrorContext, }) } case 'ConflictError': return new ConflictError(json.message, options) case 'CommitConflictError': return new CommitConflictError(json.message, options) // Resource errors case 'NotFoundError': { const ctx = json.context as { resourceType?: string; resourceId?: string } return new NotFoundError( ctx.resourceType ?? 'Resource', ctx.resourceId ?? 'unknown', { cause, context: json.context as ErrorContext } ) } case 'AlreadyExistsError': { const ctx = json.context as { resourceType?: string; resourceId?: string } return new AlreadyExistsError( ctx.resourceType ?? 'Resource', ctx.resourceId ?? 'unknown', { cause, context: json.context as ErrorContext } ) } // Rate limit errors case 'RateLimitError': { const retryAfterMs = (json.context as { retryAfterMs?: number })?.retryAfterMs const rateLimitOpts: { code: PostgresErrorCode context: ErrorContext statusCode: number retryable: boolean cause?: Error retryAfterMs?: number } = { ...options } if (retryAfterMs !== undefined) { rateLimitOpts.retryAfterMs = retryAfterMs } return new RateLimitError(json.message, rateLimitOpts) } // Migration errors case 'MigrationError': { const ctx = json.context as { migrationId?: string; phase?: string } const migrationOpts: { cause?: Error context: ErrorContext migrationId?: string phase?: string } = { context: json.context as ErrorContext } if (cause) migrationOpts.cause = cause if (ctx.migrationId !== undefined) migrationOpts.migrationId = ctx.migrationId if (ctx.phase !== undefined) migrationOpts.phase = ctx.phase return new MigrationError(json.message, migrationOpts) } // Catalog errors case 'CatalogError': return new CatalogError(json.message, options) // Circuit breaker errors case 'CircuitOpenError': { const ctx = json.context as { instanceId?: string; circuitState?: string; retryAfterMs?: number } return new CircuitOpenError( ctx.instanceId ?? '', ctx.circuitState ?? 'OPEN', ctx.retryAfterMs ?? 0 ) } // Default to base PostgresError default: return new PostgresError(json.message, options) } } /** * Aggregate multiple errors into a single batch error. * Useful for batch operations that can have multiple failures. */ static aggregate(errors: PostgresError[], message: string): AggregatePostgresError { return new AggregatePostgresError(message, errors) } /** * Reconstruct a PostgresError from a Worker response. */ static fromWorkerResponse(response: WorkerErrorResponse): PostgresError { return PostgresError.fromJSON({ name: response.__errorType, code: response.code, message: response.message, statusCode: response.statusCode, retryable: response.retryable, context: response.context, timestamp: new Date().toISOString(), }) } /** * Check if an object is a Worker error response. */ static isWorkerErrorResponse(obj: unknown): obj is WorkerErrorResponse { return ( typeof obj === 'object' && obj !== null && '__errorType' in obj && 'code' in obj && 'message' in obj ) } } // ============================================================================= // Query Errors // ============================================================================= /** * Error thrown when a query fails to execute */ export class QueryError extends PostgresError { constructor( message: string, options: { code?: PostgresErrorCode cause?: Error | unknown context?: ErrorContext sql?: string params?: unknown[] suggestion?: string } = {} ) { const context: ErrorContext = { ...options.context } if (options.sql !== undefined) context.sql = options.sql if (options.params !== undefined) context.params = options.params super(message, { code: options.code ?? 'QUERY_ERROR', cause: options.cause, context, statusCode: 400, retryable: false, suggestion: options.suggestion ?? 'Check the SQL syntax and ensure the query is valid.', }) this.name = 'QueryError' } } /** * Error thrown when a query times out */ export class QueryTimeoutError extends QueryError { readonly timeoutMs: number constructor( timeoutMs: number, options: { cause?: Error | unknown context?: ErrorContext sql?: string } = {} ) { const superOptions: { code: PostgresErrorCode cause?: unknown context?: ErrorContext sql?: string } = { code: 'QUERY_TIMEOUT' } if (options.cause !== undefined) superOptions.cause = options.cause if (options.context !== undefined) superOptions.context = options.context if (options.sql !== undefined) superOptions.sql = options.sql super(`Query timed out after ${timeoutMs}ms`, superOptions) this.name = 'QueryTimeoutError' this.timeoutMs = timeoutMs } } /** * Error thrown for constraint violations */ export class ConstraintError extends QueryError { readonly constraintType: 'unique' | 'foreign_key' | 'null' | 'check' | 'unknown' constructor( message: string, options: { constraintType?: 'unique' | 'foreign_key' | 'null' | 'check' | 'unknown' cause?: Error | unknown context?: ErrorContext sql?: string constraint?: string table?: string column?: string } = {} ) { const constraintType = options.constraintType ?? 'unknown' const codeMap: Record = { unique: 'UNIQUE_VIOLATION', foreign_key: 'FOREIGN_KEY_VIOLATION', null: 'NULL_VIOLATION', check: 'CHECK_VIOLATION', unknown: 'CONSTRAINT_VIOLATION', } const context: ErrorContext = { ...options.context } if (options.constraint !== undefined) context.constraint = options.constraint if (options.table !== undefined) context.table = options.table if (options.column !== undefined) context.column = options.column const superOptions: { code: PostgresErrorCode cause?: unknown context: ErrorContext sql?: string } = { code: codeMap[constraintType], context } if (options.cause !== undefined) superOptions.cause = options.cause if (options.sql !== undefined) superOptions.sql = options.sql super(message, superOptions) this.name = 'ConstraintError' this.constraintType = constraintType } } // ============================================================================= // Connection Errors // ============================================================================= /** * Error thrown for connection-related issues */ export class ConnectionError extends PostgresError { constructor( message: string, options: { code?: PostgresErrorCode cause?: Error | unknown context?: ErrorContext connection?: string suggestion?: string } = {} ) { // Sanitize connection string (remove passwords and sensitive data) const sanitizedConnection = options.connection ? sanitizeConnectionString(options.connection) : undefined // Sanitize the error message itself in case it contains credentials const sanitizedMessage = sanitizeErrorMessage(message) const context: ErrorContext = { ...options.context } if (sanitizedConnection) context.connection = sanitizedConnection const superOptions: { code: PostgresErrorCode cause?: unknown context: ErrorContext statusCode: number retryable: boolean suggestion: string } = { code: options.code ?? 'CONNECTION_ERROR', context, statusCode: 503, retryable: true, suggestion: options.suggestion ?? 'Check the database connection settings and ensure the database server is reachable.', } if (options.cause !== undefined) superOptions.cause = options.cause super(sanitizedMessage, superOptions) this.name = 'ConnectionError' } } /** * Error thrown when a connection times out */ export class ConnectionTimeoutError extends ConnectionError { readonly timeoutMs: number constructor( timeoutMs: number, options: { cause?: Error | unknown context?: ErrorContext connection?: string } = {} ) { const superOptions: { code: PostgresErrorCode cause?: unknown context?: ErrorContext connection?: string } = { code: 'CONNECTION_TIMEOUT' } if (options.cause !== undefined) superOptions.cause = options.cause if (options.context !== undefined) superOptions.context = options.context if (options.connection !== undefined) superOptions.connection = options.connection super(`Connection timed out after ${timeoutMs}ms`, superOptions) this.name = 'ConnectionTimeoutError' this.timeoutMs = timeoutMs } } // ============================================================================= // Validation Errors // ============================================================================= /** * Error thrown when validation fails */ export class ValidationError extends PostgresError { /** Fields that failed validation */ readonly fields: Record constructor( message: string, options: { code?: PostgresErrorCode cause?: Error | unknown context?: ErrorContext fields?: Record } = {} ) { const superOptions: { code: PostgresErrorCode cause?: unknown context?: ErrorContext statusCode: number retryable: boolean } = { code: options.code ?? 'VALIDATION_ERROR', statusCode: 400, retryable: false, } if (options.cause !== undefined) superOptions.cause = options.cause if (options.context !== undefined) superOptions.context = options.context super(message, superOptions) this.name = 'ValidationError' this.fields = options.fields ?? {} } } /** * Error thrown for invalid configuration */ export class ConfigError extends ValidationError { constructor( message: string, options: { cause?: Error | unknown context?: ErrorContext fields?: Record } = {} ) { const superOptions: { code: PostgresErrorCode cause?: unknown context?: ErrorContext fields?: Record } = { code: 'INVALID_CONFIG' } if (options.cause !== undefined) superOptions.cause = options.cause if (options.context !== undefined) superOptions.context = options.context if (options.fields !== undefined) superOptions.fields = options.fields super(message, superOptions) this.name = 'ConfigError' } } // ============================================================================= // Authentication Errors // ============================================================================= /** * Error thrown for authentication/authorization issues */ export class AuthenticationError extends PostgresError { constructor( message: string, options: { code?: PostgresErrorCode cause?: Error | unknown context?: ErrorContext } = {} ) { const code = options.code ?? 'AUTH_ERROR' const statusCodeMap: Record = { AUTH_REQUIRED: 401, AUTH_FAILED: 401, AUTH_EXPIRED: 401, AUTH_FORBIDDEN: 403, AUTH_ERROR: 401, } const superOptions: { code: PostgresErrorCode cause?: unknown context?: ErrorContext statusCode: number retryable: boolean } = { code, statusCode: statusCodeMap[code] ?? 401, retryable: false, } if (options.cause !== undefined) superOptions.cause = options.cause if (options.context !== undefined) superOptions.context = options.context super(message, superOptions) this.name = 'AuthenticationError' } } // ============================================================================= // Protocol Errors // ============================================================================= /** * Error thrown for protocol-level issues */ export class ProtocolError extends PostgresError { constructor( message: string, options: { code?: PostgresErrorCode cause?: Error | unknown context?: ErrorContext } = {} ) { const superOptions: { code: PostgresErrorCode cause?: unknown context?: ErrorContext statusCode: number retryable: boolean } = { code: options.code ?? 'PROTOCOL_ERROR', statusCode: 400, retryable: false, } if (options.cause !== undefined) superOptions.cause = options.cause if (options.context !== undefined) superOptions.context = options.context super(message, superOptions) this.name = 'ProtocolError' } } // ============================================================================= // State Errors // ============================================================================= /** * Error thrown when an operation is attempted in an invalid state */ export class StateError extends PostgresError { readonly currentState: string readonly expectedStates: string[] constructor( message: string, options: { currentState: string expectedStates: string[] cause?: Error | unknown context?: ErrorContext } ) { const context: ErrorContext = { ...options.context, currentState: options.currentState, expectedStates: options.expectedStates, } const superOptions: { code: PostgresErrorCode cause?: unknown context: ErrorContext statusCode: number retryable: boolean } = { code: 'INVALID_STATE', context, statusCode: 409, retryable: false, } if (options.cause !== undefined) superOptions.cause = options.cause super(message, superOptions) this.name = 'StateError' this.currentState = options.currentState this.expectedStates = options.expectedStates } } /** * Error thrown when a concurrency conflict occurs */ export class ConflictError extends PostgresError { constructor( message: string, options: { cause?: Error | unknown context?: ErrorContext expectedVersion?: number actualVersion?: number } = {} ) { const context: ErrorContext = { ...options.context } if (options.expectedVersion !== undefined) context.expectedVersion = options.expectedVersion if (options.actualVersion !== undefined) context.actualVersion = options.actualVersion const superOptions: { code: PostgresErrorCode cause?: unknown context: ErrorContext statusCode: number retryable: boolean } = { code: 'CONFLICT', context, statusCode: 409, retryable: true, } if (options.cause !== undefined) superOptions.cause = options.cause super(message, superOptions) this.name = 'ConflictError' } } // ============================================================================= // Resource Errors // ============================================================================= /** * Error thrown when a resource is not found */ export class NotFoundError extends PostgresError { readonly resourceType: string readonly resourceId: string constructor( resourceType: string, resourceId: string, options: { cause?: Error | unknown context?: ErrorContext } = {} ) { const context: ErrorContext = { ...options.context, resourceType, resourceId, } const superOptions: { code: PostgresErrorCode cause?: unknown context: ErrorContext statusCode: number retryable: boolean } = { code: 'NOT_FOUND', context, statusCode: 404, retryable: false, } if (options.cause !== undefined) superOptions.cause = options.cause super(`${resourceType} not found: ${resourceId}`, superOptions) this.name = 'NotFoundError' this.resourceType = resourceType this.resourceId = resourceId } } /** * Error thrown when a resource already exists */ export class AlreadyExistsError extends PostgresError { readonly resourceType: string readonly resourceId: string constructor( resourceType: string, resourceId: string, options: { cause?: Error | unknown context?: ErrorContext } = {} ) { const context: ErrorContext = { ...options.context, resourceType, resourceId, } const superOptions: { code: PostgresErrorCode cause?: unknown context: ErrorContext statusCode: number retryable: boolean } = { code: 'ALREADY_EXISTS', context, statusCode: 409, retryable: false, } if (options.cause !== undefined) superOptions.cause = options.cause super(`${resourceType} already exists: ${resourceId}`, superOptions) this.name = 'AlreadyExistsError' this.resourceType = resourceType this.resourceId = resourceId } } /** * Error thrown when rate limited */ export class RateLimitError extends PostgresError { readonly retryAfterMs: number constructor( message: string, options: { retryAfterMs?: number cause?: Error | unknown context?: ErrorContext } = {} ) { const superOptions: { code: PostgresErrorCode cause?: unknown context?: ErrorContext statusCode: number retryable: boolean } = { code: 'RATE_LIMITED', statusCode: 429, retryable: true, } if (options.cause !== undefined) superOptions.cause = options.cause if (options.context !== undefined) superOptions.context = options.context super(message, superOptions) this.name = 'RateLimitError' this.retryAfterMs = options.retryAfterMs ?? 1000 } } // ============================================================================= // Migration Errors // ============================================================================= /** * Error thrown for migration issues */ export class MigrationError extends PostgresError { readonly migrationId: string | undefined readonly phase: string | undefined constructor( message: string, options: { migrationId?: string phase?: string cause?: Error | unknown context?: ErrorContext } = {} ) { const context: ErrorContext = { ...options.context } if (options.migrationId !== undefined) context.migrationId = options.migrationId if (options.phase !== undefined) context.phase = options.phase const superOptions: { code: PostgresErrorCode cause?: unknown context: ErrorContext statusCode: number retryable: boolean } = { code: 'INTERNAL', context, statusCode: 500, retryable: false, } if (options.cause !== undefined) superOptions.cause = options.cause super(message, superOptions) this.name = 'MigrationError' this.migrationId = options.migrationId this.phase = options.phase } } // ============================================================================= // Utility Functions // ============================================================================= /** * Check if an error is a PostgresError */ export function isPostgresError(error: unknown): error is PostgresError { return error instanceof PostgresError } /** * Check if an error has a specific error code */ export function hasErrorCode(error: unknown, code: PostgresErrorCode): boolean { return isPostgresError(error) && error.code === code } /** * Check if an error is retryable */ export function isRetryableError(error: unknown): boolean { return isPostgresError(error) && error.retryable } /** * Wrap an unknown error as a PostgresError. * Automatically sanitizes error messages to remove potential credentials. * Supports optional context merging when wrapping PostgresErrors. */ export function wrapError( error: unknown, defaultMessage = 'An unexpected error occurred', options?: { context?: ErrorContext } ): PostgresError { if (error instanceof PostgresError) { // If additional context is provided, merge it with existing context if (options?.context && Object.keys(options.context).length > 0) { const mergedContext = { ...error.context, ...options.context } const newOpts: { code: PostgresErrorCode cause: PostgresError context: ErrorContext statusCode: number retryable: boolean suggestion?: string } = { code: error.code, cause: error, context: mergedContext, statusCode: error.statusCode, retryable: error.retryable, } if (error.suggestion !== undefined) { newOpts.suggestion = error.suggestion } return new PostgresError(error.message, newOpts) } return error } if (error instanceof Error) { // Sanitize the error message to remove any credentials const sanitizedMessage = sanitizeErrorMessage(error.message) || defaultMessage const errorOpts: { code: PostgresErrorCode cause: Error context?: ErrorContext } = { code: 'INTERNAL', cause: error, } if (options?.context !== undefined) { errorOpts.context = options.context } return new PostgresError(sanitizedMessage, errorOpts) } // Sanitize string errors as well const message = typeof error === 'string' ? sanitizeErrorMessage(error) : defaultMessage const context: ErrorContext = { ...options?.context } if (typeof error !== 'string') { context.originalError = error } return new PostgresError( message || defaultMessage, { code: 'INTERNAL', context, } ) } /** * Create an error from an HTTP response */ export function fromHttpResponse( status: number, body: { message?: string; code?: string; details?: Record } ): PostgresError { const message = body.message ?? `HTTP ${status} error` const context = body.details switch (status) { case 400: { const opts: { code: PostgresErrorCode; context?: ErrorContext } = { code: (body.code as PostgresErrorCode) ?? 'VALIDATION_ERROR', } if (context !== undefined) opts.context = context return new ValidationError(message, opts) } case 401: { const opts: { code: PostgresErrorCode; context?: ErrorContext } = { code: 'AUTH_REQUIRED' } if (context !== undefined) opts.context = context return new AuthenticationError(message, opts) } case 403: { const opts: { code: PostgresErrorCode; context?: ErrorContext } = { code: 'AUTH_FORBIDDEN' } if (context !== undefined) opts.context = context return new AuthenticationError(message, opts) } case 404: { const opts: { context?: ErrorContext } = {} if (context !== undefined) opts.context = context return new NotFoundError('Resource', 'unknown', opts) } case 409: { const opts: { context?: ErrorContext } = {} if (context !== undefined) opts.context = context return new ConflictError(message, opts) } case 429: { const opts: { context?: ErrorContext } = {} if (context !== undefined) opts.context = context return new RateLimitError(message, opts) } case 500: case 502: case 503: case 504: { const opts: { code: PostgresErrorCode; context?: ErrorContext } = { code: 'CONNECTION_ERROR' } if (context !== undefined) opts.context = context return new ConnectionError(message, opts) } default: { const opts: { code: PostgresErrorCode; context?: ErrorContext; statusCode: number } = { code: (body.code as PostgresErrorCode) ?? 'UNKNOWN', statusCode: status, } if (context !== undefined) opts.context = context return new PostgresError(message, opts) } } } // ============================================================================= // Identifier Validation Errors // ============================================================================= /** * Error thrown for invalid SQL identifier names. * Used by identifier validation across packages. */ export class InvalidIdentifierError extends ValidationError { readonly identifier: string readonly identifierType: 'table' | 'column' | 'schema' | string readonly reason: string constructor( name: string, type: 'table' | 'column' | 'schema' | string, reason: string ) { super(`Invalid ${type} name "${name}": ${reason}`, { code: 'VALIDATION_ERROR', context: { identifier: name, identifierType: type, reason }, fields: { [type]: reason }, }) this.name = 'InvalidIdentifierError' this.identifier = name this.identifierType = type this.reason = reason } } // ============================================================================= // Circuit Breaker Errors // ============================================================================= /** * Error thrown when a circuit breaker is open and requests are rejected. */ export class CircuitOpenError extends PostgresError { readonly instanceId: string readonly circuitState: string readonly retryAfterMs: number constructor( instanceId: string, state: string, retryAfterMs: number ) { super(`Circuit breaker is ${state} for instance ${instanceId}. Retry after ${retryAfterMs}ms`, { code: 'RATE_LIMITED', context: { instanceId, circuitState: state, retryAfterMs }, statusCode: 503, retryable: true, }) this.name = 'CircuitOpenError' this.instanceId = instanceId this.circuitState = state this.retryAfterMs = retryAfterMs } } // ============================================================================= // Tenant Routing Errors // ============================================================================= /** * Error thrown when a tenant cannot be identified from a request. */ export class TenantNotFoundError extends NotFoundError { constructor(message: string = 'Tenant identifier not found in request') { super('Tenant', 'unknown', { context: { reason: message } }) this.name = 'TenantNotFoundError' // Override the message to match the original format Object.defineProperty(this, 'message', { value: message, writable: true, configurable: true }) } } /** * Error thrown when a tenant is blocked from accessing resources. */ export class TenantBlockedError extends AuthenticationError { readonly tenantId: string constructor(tenantId: string, message: string = 'Tenant is blocked') { super(message, { code: 'AUTH_FORBIDDEN', context: { tenantId }, }) this.name = 'TenantBlockedError' this.tenantId = tenantId } } /** * Error thrown when a tenant ID format is invalid. */ export class InvalidTenantIdError extends ValidationError { readonly tenantId: string constructor(tenantId: string, message: string = 'Invalid tenant identifier format') { super(message, { code: 'INVALID_FORMAT', context: { tenantId }, fields: { tenantId: 'Invalid format' }, }) this.name = 'InvalidTenantIdError' this.tenantId = tenantId } } /** * Error thrown when a tenant DO request times out. */ export class TenantTimeoutError extends ConnectionTimeoutError { readonly tenantId: string constructor( tenantId: string, timeoutMs: number, message: string = 'Tenant DO request timed out' ) { super(timeoutMs, { context: { tenantId } }) this.name = 'TenantTimeoutError' this.tenantId = tenantId // Override the message to match the original format Object.defineProperty(this, 'message', { value: message, writable: true, configurable: true }) } } /** * Error thrown when a tenant DO is unreachable. */ export class TenantUnavailableError extends ConnectionError { readonly tenantId: string constructor( tenantId: string, cause?: Error, message: string = 'Tenant DO is unavailable' ) { const opts: { code: 'CONNECTION_ERROR' cause?: Error context: ErrorContext } = { code: 'CONNECTION_ERROR', context: { tenantId }, } if (cause !== undefined) opts.cause = cause super(message, opts) this.name = 'TenantUnavailableError' this.tenantId = tenantId } } /** * Error thrown when a tenant exceeds rate limits. */ export class TenantRateLimitError extends RateLimitError { readonly tenantId: string constructor( tenantId: string, retryAfterSeconds: number, message: string = 'Rate limit exceeded' ) { super(message, { retryAfterMs: retryAfterSeconds * 1000, context: { tenantId }, }) this.name = 'TenantRateLimitError' this.tenantId = tenantId } } // ============================================================================= // Write Blocking Errors // ============================================================================= /** * Error thrown when a write operation is blocked in read-only mode. */ export class WriteBlockedError extends QueryError { /** PostgreSQL SQLSTATE code for read-only transaction */ readonly pgCode: string = '25006' constructor(sql: string) { super('Write operation blocked: database is in read-only mode', { code: 'INVALID_STATE', sql, }) this.name = 'WriteBlockedError' } } // ============================================================================= // Recovery and Snapshot Errors // ============================================================================= /** * Error thrown when a snapshot is not found during recovery. */ export class SnapshotNotFoundError extends NotFoundError { constructor(snapshotId: string) { super('Snapshot', snapshotId) this.name = 'SnapshotNotFoundError' } } /** * Error thrown when recovery operation limits are exceeded. */ export class RecoveryLimitExceededError extends PostgresError { readonly limit: number readonly actual: number constructor( message: string, options: { limit: number actual: number context?: ErrorContext } ) { super(message, { code: 'QUOTA_EXCEEDED', context: { ...options.context, limit: options.limit, actual: options.actual, }, statusCode: 429, retryable: false, }) this.name = 'RecoveryLimitExceededError' this.limit = options.limit this.actual = options.actual } } /** * Error thrown when recovery verification fails. */ export class RecoveryVerificationError extends PostgresError { constructor( message: string, options: { cause?: Error context?: ErrorContext } = {} ) { const superOptions: { code: 'INTERNAL' cause?: Error context?: ErrorContext statusCode: number retryable: boolean } = { code: 'INTERNAL', statusCode: 500, retryable: false, } if (options.cause !== undefined) superOptions.cause = options.cause if (options.context !== undefined) superOptions.context = options.context super(message, superOptions) this.name = 'RecoveryVerificationError' } } // ============================================================================= // Time Travel Errors // ============================================================================= /** * Error thrown when a timestamp is outside the valid range for time travel. */ export class TimestampOutOfRangeError extends ValidationError { readonly queryTimestamp: number readonly minTimestamp: number readonly maxTimestamp: number constructor( timestamp: number, minTimestamp: number, maxTimestamp: number, message?: string ) { super(message ?? `Timestamp ${timestamp} is outside the valid range [${minTimestamp}, ${maxTimestamp}]`, { code: 'VALIDATION_ERROR', context: { timestamp, minTimestamp, maxTimestamp }, fields: { timestamp: 'Out of range' }, }) this.name = 'TimestampOutOfRangeError' this.queryTimestamp = timestamp this.minTimestamp = minTimestamp this.maxTimestamp = maxTimestamp } } /** * Error thrown when no snapshot is found for a time travel query. */ export class NoSnapshotFoundError extends NotFoundError { readonly queryTimestamp: number constructor(timestamp: number, message?: string) { super('Snapshot', `at timestamp ${timestamp}`) this.name = 'NoSnapshotFoundError' this.queryTimestamp = timestamp if (message) { Object.defineProperty(this, 'message', { value: message, writable: true, configurable: true }) } } } /** * Error thrown for invalid timestamp formats. */ export class InvalidTimestampError extends ValidationError { readonly value: unknown constructor(value: unknown, message?: string) { super(message ?? `Invalid timestamp: ${String(value)}`, { code: 'INVALID_FORMAT', context: { value: String(value) }, fields: { timestamp: 'Invalid format' }, }) this.name = 'InvalidTimestampError' this.value = value } } // ============================================================================= // Catalog Errors (Iceberg/Data Lake) // ============================================================================= /** * Error thrown for catalog operations (Iceberg, data lake). */ export class CatalogError extends PostgresError { constructor( message: string, options: { code?: PostgresErrorCode cause?: Error context?: ErrorContext statusCode?: number retryable?: boolean } = {} ) { const superOptions: { code: PostgresErrorCode cause?: Error context?: ErrorContext statusCode: number retryable: boolean } = { code: options.code ?? 'INTERNAL', statusCode: options.statusCode ?? 500, retryable: options.retryable ?? false, } if (options.cause !== undefined) superOptions.cause = options.cause if (options.context !== undefined) superOptions.context = options.context super(message, superOptions) this.name = 'CatalogError' } } /** * Error thrown when a commit conflict occurs in the catalog. */ export class CommitConflictError extends ConflictError { constructor( message: string, options: { cause?: Error context?: ErrorContext expectedVersion?: number actualVersion?: number } = {} ) { super(message, options) this.name = 'CommitConflictError' } } /** * Error thrown when a table is not found in the catalog. */ export class CatalogTableNotFoundError extends NotFoundError { constructor(tableName: string, namespace?: string) { const resourceId = namespace ? `${namespace}.${tableName}` : tableName super('Table', resourceId) this.name = 'CatalogTableNotFoundError' } } /** * Error thrown when a namespace is not found in the catalog. */ export class NamespaceNotFoundError extends NotFoundError { constructor(namespace: string) { super('Namespace', namespace) this.name = 'NamespaceNotFoundError' } } // ============================================================================= // Pool Errors // ============================================================================= /** * Error thrown for connection pool issues. */ export class PoolError extends ConnectionError { constructor( message: string, options: { code?: PostgresErrorCode cause?: Error context?: ErrorContext } = {} ) { const superOptions: { code: PostgresErrorCode cause?: Error context?: ErrorContext } = { code: options.code ?? 'CONNECTION_ERROR', } if (options.cause !== undefined) superOptions.cause = options.cause if (options.context !== undefined) superOptions.context = options.context super(message, superOptions) this.name = 'PoolError' } } // ============================================================================= // Aggregate Errors // ============================================================================= /** * Error that aggregates multiple PostgresErrors. * Useful for batch operations that can have multiple failures. */ export class AggregatePostgresError extends PostgresError { /** The individual errors that were aggregated */ readonly errors: PostgresError[] constructor(message: string, errors: PostgresError[]) { // Collect unique error codes const errorCodes = [...new Set(errors.map(e => e.code))] // Determine if any error is retryable const retryable = errors.some(e => e.retryable) super(message, { code: 'INTERNAL', context: { failedCount: errors.length, errorCodes, }, statusCode: 500, retryable, }) this.name = 'AggregatePostgresError' this.errors = errors } override toJSON(): StandardErrorJSON & { errors: StandardErrorJSON[] } { const baseJson = super.toJSON() return { ...baseJson, errors: this.errors.map(e => e.toJSON()), } } } // ============================================================================= // Type Guard Functions // ============================================================================= /** * Type guard to check if an error is a QueryError */ export function isQueryError(error: unknown): error is QueryError { return isPostgresError(error) && error instanceof QueryError } /** * Type guard to check if an error is a ConnectionError */ export function isConnectionError(error: unknown): error is ConnectionError { return isPostgresError(error) && error instanceof ConnectionError } /** * Type guard to check if an error is a ValidationError */ export function isValidationError(error: unknown): error is ValidationError { return isPostgresError(error) && error instanceof ValidationError } /** * Type guard to check if an error is an AuthenticationError */ export function isAuthenticationError(error: unknown): error is AuthenticationError { return isPostgresError(error) && error instanceof AuthenticationError } /** * Type guard to check if an error is a RateLimitError */ export function isRateLimitError(error: unknown): error is RateLimitError { return isPostgresError(error) && error instanceof RateLimitError } /** * Type guard to check if an error is a NotFoundError */ export function isNotFoundError(error: unknown): error is NotFoundError { return isPostgresError(error) && error instanceof NotFoundError } /** * Type guard to check if an error is a ConflictError */ export function isConflictError(error: unknown): error is ConflictError { return isPostgresError(error) && error instanceof ConflictError } /** * Narrow an error to a specific type based on its error code. * Returns the error with the narrowed type if the code matches. */ export function narrowByErrorCode( error: unknown, code: T ): (PostgresError & { code: T }) | undefined { if (isPostgresError(error) && error.code === code) { return error as PostgresError & { code: T } } return undefined }