pg
semver
>=8.0.0postconditions27functions5last verified2026-06-23coverage score83%Postconditions — what we check
- query · syntax-errorerrorWhenSQL syntax error (42601)Throws
Error with code '42601'Required handlingCaller MUST validate SQL syntax before execution. This indicates a SQL syntax error - DO NOT retry. Check error.position for location of syntax error in query.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[1] - query · unique-violationerrorWhenUnique constraint violation (23505)Throws
Error with code '23505'Required handlingCaller MUST handle duplicate key violations gracefully. Extract constraint name from error.constraint. DO NOT retry without changing the unique field value.costhighin prodimmediate exceptionusers seelost datavisibilityvisibleSources[1] - query · foreign-key-violationerrorWhenForeign key constraint violation (23503)Throws
Error with code '23503'Required handlingCaller MUST verify referenced record exists before insertion. Extract constraint and table from error.constraint and error.table. This indicates data integrity issue - DO NOT retry.costhighin prodimmediate exceptionusers seelost datavisibilityvisibleSources[1] - query · not-null-violationerrorWhenNOT NULL constraint violation (23502)Throws
Error with code '23502'Required handlingCaller MUST provide all required fields. Extract column name from error.column. Validate data completeness before query execution.costmediumin prodimmediate exceptionusers seelost datavisibilityvisibleSources[1] - query · connection-errorerrorWhenConnection to database failedThrows
Error with code 'ECONNREFUSED' or similar network errorsRequired handlingCaller MUST handle connection failures separately from query errors. Common causes: database down, wrong host/port, network issues. Implement retry with exponential backoff for transient connection issues. Alert operations if connection errors persist.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[2] - query · undefined-tableerrorWhenTable or view does not exist (42P01)Throws
Error with code '42P01'Required handlingCaller MUST verify table exists before querying. Check error.message for table name. DO NOT retry - this indicates schema mismatch or missing migration.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[1] - query · null-undefined-query-throwserrorWhenclient.query() or pool.query() called with null/undefined first argumentThrows
TypeError with message 'Client was passed a null or undefined query'Required handlingCaller MUST validate that the query string/config is non-null before invoking query(). Common cause: building dynamic SQL from a conditional that may yield undefined (e.g. `const sql = condition ? buildQuery() : undefined; await client.query(sql)`). This is a synchronous TypeError thrown before any Promise resolution — it WILL NOT be caught by `.catch()` on the returned promise if the caller forgot to await inside try/catch. Wrap query() invocations in try/catch when the query source could be undefined.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[3] - query · pool-query-function-arg-rejectserrorWhenpool.query() called with a function as the first argumentThrows
Error with message 'Passing a function as the first parameter to pool.query is not supported'Required handlingCaller MUST pass a query string or QueryConfig object as the first argument to pool.query() — never a function. Common cause: passing a query-builder factory by reference instead of calling it. Pattern check: `pool.query(buildQuery, ...)` should be `pool.query(buildQuery(), ...)`. The error rejects the returned Promise — must be handled in catch block.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - query · query-read-timeouterrorWhenQuery exceeds query_timeout (per-query or client-level) millisecondsThrows
Error with message 'Query read timeout'Required handlingCaller MUST handle query timeout errors distinctly from connection errors. When query_timeout is configured (on Client config or per-query via QueryConfig), long-running queries that exceed the timeout reject with 'Query read timeout'. DO NOT blindly retry — the query may still be running on the database and a retry can cause duplicate work or duplicate writes. Cancel the query at the PostgreSQL level via pg_cancel_backend() if retry is required. For idempotent read queries, retry with shorter timeout is acceptable.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - query · database-error-canonical-typewarningWhenPostgres server-side error (constraint violation, syntax error, type error, etc.)Throws
DatabaseError (from 'pg-protocol', re-exported by pg) with .code property containing SQLSTATERequired handlingCaller SHOULD use `error instanceof DatabaseError` to distinguish Postgres-level errors from client-side errors (TypeError, network errors). DatabaseError exposes structured fields not available on generic Error: .code (5-char SQLSTATE), .detail, .hint, .position (for syntax errors), .constraint, .table, .column, .schema. Generic `catch (error)` without instance check loses this structured context. Pattern: try { await client.query(...); } catch (err) { if (err instanceof DatabaseError) { // handle by err.code (e.g. '23505' unique violation) } else { // network / type error } }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - connect · pool-exhaustederrorWhenAll clients in pool are busy and connectionTimeoutMillis exceededThrows
Error with message 'timeout exceeded when trying to connect'Required handlingCaller MUST handle pool exhaustion errors. Root causes: 1. Clients not released (forgot client.release()) 2. Pool size too small for load 3. Queries taking too long Implement proper client.release() in finally blocks. Monitor pool metrics to identify leaks.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[5] - connect · connection-errorerrorWhenCannot establish database connectionThrows
Error with code 'ECONNREFUSED', 'ETIMEDOUT', etc.Required handlingCaller MUST handle connection failures. Implement retry with exponential backoff. Check database server status. Verify connection string credentials.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[9] - connect · client-not-releasederrorWhenCaller forgets to release client back to poolThrows
No error thrown, but pool becomes exhausted over timeRequired handlingCaller MUST ALWAYS release clients in a finally block: ``` const client = await pool.connect(); try { await client.query(...); } finally { client.release(); } ``` CRITICAL: This is the #1 production bug (affects 30% of mid-sized projects). Failure to release causes pool exhaustion and application hangs. Silent failure until complete outage.costcriticalin proddelayed failureusers seeservice unavailablevisibilityvisibleSources[10] - connect · client-reuse-after-connecterrorWhenClient.connect() called a second time on the same Client instanceThrows
Error with message 'Client has already been connected. You cannot reuse a client.'Required handlingCaller MUST construct a NEW Client instance for each connection cycle. Calling .connect() twice on the same Client (even after .end()) rejects with this error. Common cause: connection-retry logic that loops on the same client instance after end(). CORRECT pattern: on retry, instantiate `const client = new Client(config)` then await client.connect(). For pooled scenarios, use Pool which manages the Client lifecycle internally.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - connect · pool-connect-after-enderrorWhenPool.connect() called after Pool.end() has been initiatedThrows
Error with message 'Cannot use a pool after calling end on the pool'Required handlingCaller MUST stop routing new request handlers before calling pool.end(). Pattern: set a `shuttingDown` flag in the SIGTERM/SIGINT handler, reject incoming requests while shuttingDown is true, then call pool.end(). Without this guard, any in-flight request handler that calls pool.connect() during graceful-shutdown drain throws this error and the request fails. This is distinct from the `pool-query-after-end` postcondition on `end` — that one covers pool.query(); this one covers pool.connect() specifically.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[4] - connect · pool-onconnect-hook-failserrorWhenPoolConfig.onConnect callback throws or rejects during connection setupThrows
The hook's own error (any Error subclass) rejected from Pool.connect()Required handlingCaller MUST handle errors from pool.connect() even when an onConnect hook is configured — a failing hook propagates as a pool.connect() rejection. The onConnect hook is invoked after the underlying TCP connect succeeds but before the client is returned. Common failure modes inside onConnect: (1) a SET command fails (`SET search_path TO ...` with an invalid schema), (2) a session-config query times out, (3) the hook awaits an external service that returns 500. The pool destroys the client on hook failure (client.end()) — partial initialization is not retained. Implement retry logic around pool.connect() calls whose onConnect performs network I/O.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - connect · connection-terminated-during-timeouterrorWhenTCP/TLS handshake for a new pool client exceeds connectionTimeoutMillisThrows
Error with message 'Connection terminated due to connection timeout' (with original error attached as .cause)Required handlingCaller MUST distinguish this from `pool-exhausted` (which fires when an existing idle client is unavailable). This timeout fires during the actual TCP/TLS handshake when establishing a new connection — typically caused by network latency, firewall delays, or a slow PostgreSQL bootstrap (slow SSL negotiation on PG 17+ direct sslnegotiation). Inspect error.cause for the underlying connect error to diagnose. Treat as a transient error and retry with backoff, but escalate to ops if persistent — usually indicates network/infrastructure issue, not pool sizing.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[4] - connect · sslnegotiation-direct-requires-sslerrorWhenPool/Client constructed with sslnegotiation: 'direct' but ssl is falsyThrows
Error with message 'sslnegotiation=direct requires SSL to be enabled' (or 'Invalid sslnegotiation value: ...' for unknown values)Required handlingCaller MUST set ssl: true (or an ssl ConnectionOptions object) when using sslnegotiation: 'direct'. Direct SSL negotiation requires TLS — there is no plaintext mode. This is a configuration-time error: the Pool or Client constructor throws synchronously, so the failure occurs at app startup, not on first connect(). When upgrading to pg@8.22.0+ and PostgreSQL 17+ for the faster TLS handshake, audit existing PoolConfig/ClientConfig to ensure ssl is set wherever sslnegotiation is. Equally, 'sslnegotiation' values other than 'postgres' or 'direct' throw a similar Invalid-value error.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - end · pool-end-called-twiceerrorWhenPool.end() called more than once on the same pool instanceThrows
Error with message 'Called end on pool more than once'Required handlingCaller MUST guard against double-end by checking a boolean flag before calling pool.end(). Common in serverless functions with multiple shutdown signal handlers. Pattern: if (!isShuttingDown) { isShuttingDown = true; await pool.end(); } Failure to guard causes an unhandled rejection on the second call.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - end · pool-query-after-enderrorWhenPool.connect() or Pool.query() called after Pool.end() has completedThrows
Error with message 'Cannot use a pool after calling end on the pool'Required handlingCaller MUST stop routing new requests before initiating pool shutdown. Implement graceful drain: stop accepting requests → wait for in-flight to complete → then call pool.end(). Never call pool.end() in the middle of active request handling without draining first.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - end · client-end-connection-terminatedwarningWhenQueries in-flight when Client.end() is calledThrows
Error with message 'Client was closed and is not queryable'Required handlingCaller MUST ensure no queries are in-flight before calling client.end(). For standalone client usage, always await all queries to completion before calling end(). Use try/finally to ensure end() is called after query completion.costhighin prodimmediate exceptionusers seelost datavisibilityvisible - release · double-release-throwserrorWhenclient.release() called more than once on the same PoolClientThrows
Error with message 'Release called on client which has already been released to the pool.'Required handlingCaller MUST call release() exactly once. ALWAYS use finally block exclusively: const client = await pool.connect(); try { ... } finally { client.release(); } NEVER call release() in both catch and finally — the catch error path goes through the finally block, causing a double-release. If a transaction error occurred, call client.release(err) once in finally.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - release · release-with-error-destroys-clientwarningWhenclient.release(err) called with an error after a failed transactionThrows
No error thrown — client is destroyed instead of returned to poolRequired handlingCaller SHOULD pass the error to release() when the client was used in a failed transaction: client.release(err). This destroys the connection rather than returning a tainted connection (with an open transaction or held locks) to the pool. Forgetting to pass the error can cause the next user of that pooled connection to inherit transaction state.costhighin prodimmediate exceptionusers seelost datavisibilityvisible - transaction · transaction-not-rolled-back-on-errorerrorWhenQuery inside BEGIN/COMMIT transaction block throws and ROLLBACK is not issuedThrows
No error from pg — but PostgreSQL holds row locks until session disconnectsRequired handlingCaller MUST issue ROLLBACK in the catch block whenever a transaction is in progress. REQUIRED pattern: try { await client.query('BEGIN'); ... await client.query('COMMIT'); } catch (e) { await client.query('ROLLBACK'); throw e; } finally { client.release(); } Failure to ROLLBACK leaves open locks, causing other queries to block waiting for those rows. Can cascade into application-wide deadlock.costcriticalin proddelayed failureusers seeservice unavailablevisibilityvisible - transaction · transaction-on-pool-queryerrorWhenpool.query() used for BEGIN/COMMIT/ROLLBACK instead of a dedicated clientThrows
No error thrown — but transaction silently spans different connectionsRequired handlingCaller MUST obtain a dedicated client via pool.connect() for all transaction work. NEVER use pool.query() for BEGIN/COMMIT/ROLLBACK — each call may hit a different connection, making the transaction a no-op. CORRECT: const client = await pool.connect(); await client.query('BEGIN'); ... WRONG: await pool.query('BEGIN'); await pool.query(...); await pool.query('COMMIT');costcriticalin prodsilent failureusers seelost datavisibilitysilentSources[12] - transaction · transaction-deadlockerrorWhenTwo concurrent transactions deadlock — PostgreSQL code 40P01Throws
DatabaseError with error.code === '40P01' (deadlock_detected)Required handlingCaller MUST catch DatabaseError with code '40P01' and retry the entire transaction from BEGIN. DO NOT retry individual queries — the whole transaction context is invalidated. Implement exponential backoff with jitter between retries. Check error.code to distinguish deadlock from other errors.costhighin prodimmediate exceptionusers seelost datavisibilityvisible - transaction · transaction-serialization-failureerrorWhenSERIALIZABLE transaction conflicts — PostgreSQL code 40001Throws
DatabaseError with error.code === '40001' (serialization_failure)Required handlingCaller MUST catch DatabaseError with code '40001' and retry the entire transaction from BEGIN. Serialization failures are expected in SERIALIZABLE isolation — they are not bugs but rather PostgreSQL enforcing correctness. Implement retry with backoff. Only applies when using SERIALIZABLE isolation.costhighin prodimmediate exceptionusers seelost datavisibilityvisible
Sources
Every postcondition cites at least one of these. Numbered to match the footnotes above.
- [1]postgresql.org/docs/currenthttps://www.postgresql.org/docs/current/errcodes-appendix.html
- [2]node-postgres.comhttps://node-postgres.com/
- [3]github.com/brianc/node-postgreshttps://github.com/brianc/node-postgres/blob/master/lib/client.js
- [4]github.com/brianc/node-postgreshttps://github.com/brianc/node-postgres/blob/master/packages/pg-pool/index.js
- [5]node-postgres.com/apis/poolhttps://node-postgres.com/apis/pool
- [6]node-postgres.com/announcementshttps://node-postgres.com/announcements
- [7]github.com/brianc/node-postgreshttps://github.com/brianc/node-postgres/blob/master/packages/pg-protocol/src/messages.ts
- [8]node-postgres.com/apis/clienthttps://node-postgres.com/apis/client
- [9]node-postgres.com/features/poolinghttps://node-postgres.com/features/pooling
- [10]github.com/brianc/node-postgreshttps://github.com/brianc/node-postgres/issues/1882
- [11]github.com/brianc/node-postgreshttps://github.com/brianc/node-postgres/blob/master/lib/connection-parameters.js
- [12]node-postgres.com/features/transactionshttps://node-postgres.com/features/transactions
- [13]postgresql.org/docs/currenthttps://www.postgresql.org/docs/current/sql-rollback.html
- [14]postgresql.org/docs/currenthttps://www.postgresql.org/docs/current/explicit-locking.html
- [15]postgresql.org/docs/currenthttps://www.postgresql.org/docs/current/transaction-iso.html
Need a different package?
Request a profile