From 61c8a07316b65c9ab0594db12050eb4662c900a7 Mon Sep 17 00:00:00 2001 From: Yeongbin Jeong Date: Thu, 5 Nov 2026 03:04:00 +0900 Subject: [PATCH] feat: add codes to errors created by pg and pg-pool Errors from the server carry the SQLSTATE in `code`, but errors that the client creates itself (connection and query timeouts, a terminated connection, SSL refusal) had none, so callers had to match messages. Give the runtime ones an `ERR_PG_` code and document them. Messages stay the same. Errors for library misuse are left without a code. Closes #3380 --- docs/pages/apis/client.mdx | 25 +++++++++++++++++++ docs/pages/apis/pool.mdx | 9 +++++++ packages/pg-pool/index.js | 8 ++++-- packages/pg-pool/test/connection-timeout.js | 13 ++++++++++ packages/pg-pool/test/error-handling.js | 10 +++----- packages/pg/lib/client.js | 8 +++--- packages/pg/lib/connection.js | 14 +++++++++-- packages/pg/lib/native/client.js | 4 +-- .../client/connection-timeout-tests.js | 4 +++ .../test/unit/client/query-timeout-tests.js | 2 ++ ...tream-and-query-error-interaction-tests.js | 3 ++- .../pg/test/unit/connection/error-tests.js | 4 +++ 12 files changed, 88 insertions(+), 16 deletions(-) diff --git a/docs/pages/apis/client.mdx b/docs/pages/apis/client.mdx index 973b1a832..25a06fec1 100644 --- a/docs/pages/apis/client.mdx +++ b/docs/pages/apis/client.mdx @@ -242,6 +242,31 @@ try { await client.end() ``` +## error codes + +Errors sent by the PostgreSQL server have the server's [SQLSTATE](https://www.postgresql.org/docs/current/errcodes-appendix.html) in `err.code`, and network errors keep Node's own codes such as `ECONNREFUSED`. Errors that the client creates itself have a code starting with `ERR_PG_`, so you can check `err.code` instead of the message: + +| code | when | +| ------------------------------- | --------------------------------------------------------------------------- | +| `ERR_PG_CONNECTION_TIMEOUT` | the connection was not established within `connectionTimeoutMillis` | +| `ERR_PG_CONNECTION_TERMINATED` | the connection ended unexpectedly, for example because the server closed it | +| `ERR_PG_CONNECTION_ENDED` | a query was still pending when `client.end()` was called | +| `ERR_PG_QUERY_TIMEOUT` | a query did not finish within `query_timeout` | +| `ERR_PG_SSL_NOT_SUPPORTED` | `ssl` is enabled but the server does not support SSL connections | +| `ERR_PG_SSL_NEGOTIATION_FAILED` | the server answered the SSL request with something other than yes or no | + +With the [native bindings](/features/native), connection and SSL errors come from libpq and have no code; `ERR_PG_QUERY_TIMEOUT` and `ERR_PG_CONNECTION_ENDED` are set by both clients. + +```js +try { + await client.query('SELECT pg_sleep(10)') +} catch (err) { + if (err.code === 'ERR_PG_QUERY_TIMEOUT') { + // retry, or report a timeout + } +} +``` + ## events ### error diff --git a/docs/pages/apis/pool.mdx b/docs/pages/apis/pool.mdx index 3627dd1c5..f0e5b2dd9 100644 --- a/docs/pages/apis/pool.mdx +++ b/docs/pages/apis/pool.mdx @@ -229,6 +229,15 @@ The number of clients which are not checked out but are currently idle in the po The number of queued requests waiting on a client when all clients are checked out. It can be helpful to monitor this number to see if you need to adjust the size of the pool. +## error codes + +Besides the [client's error codes](/apis/client#error-codes), the pool sets these codes on the errors it creates itself: + +| code | when | +| ------------------------------ | ------------------------------------------------------------------------------------------------------ | +| `ERR_PG_POOL_CONNECT_TIMEOUT` | a new client did not connect within `connectionTimeoutMillis`; the client's error is in `err.cause` | +| `ERR_PG_POOL_CHECKOUT_TIMEOUT` | no client was released to the waiting request within `connectionTimeoutMillis` while the pool was full | + ## events `Pool` instances are also instances of [`EventEmitter`](https://nodejs.org/api/events.html). diff --git a/packages/pg-pool/index.js b/packages/pg-pool/index.js index ab514fa88..2fc194570 100644 --- a/packages/pg-pool/index.js +++ b/packages/pg-pool/index.js @@ -221,7 +221,9 @@ class Pool extends EventEmitter { // we're going to call it with a timeout error removeWhere(this._pendingQueue, (i) => i.callback === queueCallback) pendingItem.timedOut = true - response.callback(new Error('timeout exceeded when trying to connect')) + response.callback( + Object.assign(new Error('timeout exceeded when trying to connect'), { code: 'ERR_PG_POOL_CHECKOUT_TIMEOUT' }) + ) }, this.options.connectionTimeoutMillis) if (tid.unref) { @@ -273,7 +275,9 @@ class Pool extends EventEmitter { // remove the dead client from our list of clients this._clients = this._clients.filter((c) => c !== client) if (timeoutHit) { - err = new Error('Connection terminated due to connection timeout', { cause: err }) + err = Object.assign(new Error('Connection terminated due to connection timeout', { cause: err }), { + code: 'ERR_PG_POOL_CONNECT_TIMEOUT', + }) } // this client won’t be released, so move on immediately diff --git a/packages/pg-pool/test/connection-timeout.js b/packages/pg-pool/test/connection-timeout.js index c4fd1832b..3dc1eb73e 100644 --- a/packages/pg-pool/test/connection-timeout.js +++ b/packages/pg-pool/test/connection-timeout.js @@ -35,6 +35,7 @@ describe('connection timeout', () => { pool.connect((err, client, release) => { expect(err).to.be.an(Error) expect(err.message).to.contain('timeout') + expect(err.code).to.equal('ERR_PG_POOL_CONNECT_TIMEOUT') expect(client).to.equal(undefined) expect(pool.idleCount).to.equal(0) done() @@ -46,11 +47,23 @@ describe('connection timeout', () => { pool.connect().catch((err) => { expect(err).to.be.an(Error) expect(err.message).to.contain('timeout') + expect(err.code).to.equal('ERR_PG_POOL_CONNECT_TIMEOUT') expect(pool.idleCount).to.equal(0) done() }) }) + it('should give a checkout timeout its own code when the pool is full', (done) => { + const pool = new Pool({ connectionTimeoutMillis: 50, max: 1, port: this.port, host: 'localhost' }) + pool.connect(() => {}) // takes the only slot; never finishes connecting to the silent server + pool.connect((err, client) => { + expect(err.message).to.equal('timeout exceeded when trying to connect') + expect(err.code).to.equal('ERR_PG_POOL_CHECKOUT_TIMEOUT') + expect(client).to.equal(undefined) + done() + }) + }) + it( 'should handle multiple timeouts', co.wrap( diff --git a/packages/pg-pool/test/error-handling.js b/packages/pg-pool/test/error-handling.js index 60354325c..f0239cb26 100644 --- a/packages/pg-pool/test/error-handling.js +++ b/packages/pg-pool/test/error-handling.js @@ -227,15 +227,13 @@ describe('pool error handling', function () { const pool = new Pool({ max: 1, port: closeServer.address().port, host: 'localhost' }) pool.connect((err) => { expect(err).to.be.an(Error) - if (err.code) { - expect(err.code).to.be('ECONNRESET') - } + // the socket is either reset or closed cleanly, depending on the platform + expect(['ECONNRESET', 'ERR_PG_CONNECTION_TERMINATED']).to.contain(err.code) }) pool.connect((err) => { expect(err).to.be.an(Error) - if (err.code) { - expect(err.code).to.be('ECONNRESET') - } + // the socket is either reset or closed cleanly, depending on the platform + expect(['ECONNRESET', 'ERR_PG_CONNECTION_TERMINATED']).to.contain(err.code) closeServer.close(() => { pool.end(done) }) diff --git a/packages/pg/lib/client.js b/packages/pg/lib/client.js index 2b13c1de7..213f2d39e 100644 --- a/packages/pg/lib/client.js +++ b/packages/pg/lib/client.js @@ -167,7 +167,7 @@ class Client extends EventEmitter { if (this._connectionTimeoutMillis > 0) { this.connectionTimeoutHandle = setTimeout(() => { con._ending = true - con.stream.destroy(new Error('timeout expired')) + con.stream.destroy(Object.assign(new Error('timeout expired'), { code: 'ERR_PG_CONNECTION_TIMEOUT' })) }, this._connectionTimeoutMillis) if (this.connectionTimeoutHandle.unref) { @@ -201,7 +201,9 @@ class Client extends EventEmitter { this._attachListeners(con) con.once('end', () => { - const error = this._ending ? new Error('Connection terminated') : new Error('Connection terminated unexpectedly') + const error = this._ending + ? Object.assign(new Error('Connection terminated'), { code: 'ERR_PG_CONNECTION_ENDED' }) + : Object.assign(new Error('Connection terminated unexpectedly'), { code: 'ERR_PG_CONNECTION_TERMINATED' }) clearTimeout(this.connectionTimeoutHandle) this._errorAllQueries(error) @@ -704,7 +706,7 @@ class Client extends EventEmitter { const queryCallback = query.callback || (() => {}) const readTimeoutTimer = setTimeout(() => { - const error = new Error('Query read timeout') + const error = Object.assign(new Error('Query read timeout'), { code: 'ERR_PG_QUERY_TIMEOUT' }) process.nextTick(() => { query.handleError(error, this.connection) diff --git a/packages/pg/lib/connection.js b/packages/pg/lib/connection.js index 85f3113a1..48d20d82a 100644 --- a/packages/pg/lib/connection.js +++ b/packages/pg/lib/connection.js @@ -83,11 +83,21 @@ class Connection extends EventEmitter { break case 'N': // Server does not support SSL connections self.stream.end() - return self.emit('error', new Error('The server does not support SSL connections')) + return self.emit( + 'error', + Object.assign(new Error('The server does not support SSL connections'), { + code: 'ERR_PG_SSL_NOT_SUPPORTED', + }) + ) default: // Any other response byte, including 'E' (ErrorResponse) indicating a server error self.stream.end() - return self.emit('error', new Error('There was an error establishing an SSL connection')) + return self.emit( + 'error', + Object.assign(new Error('There was an error establishing an SSL connection'), { + code: 'ERR_PG_SSL_NEGOTIATION_FAILED', + }) + ) } self.upgradeToSSL(host, reportStreamError) }) diff --git a/packages/pg/lib/native/client.js b/packages/pg/lib/native/client.js index 9ec3c8c03..d192cb797 100644 --- a/packages/pg/lib/native/client.js +++ b/packages/pg/lib/native/client.js @@ -193,7 +193,7 @@ Client.prototype.query = function (config, values, callback) { queryCallback = query.callback || (() => {}) readTimeoutTimer = setTimeout(() => { - const error = new Error('Query read timeout') + const error = Object.assign(new Error('Query read timeout'), { code: 'ERR_PG_QUERY_TIMEOUT' }) process.nextTick(() => { query.handleError(error, this.connection) @@ -267,7 +267,7 @@ Client.prototype.end = function (cb) { self.native.end(function () { self._connected = false - self._errorAllQueries(new Error('Connection terminated')) + self._errorAllQueries(Object.assign(new Error('Connection terminated'), { code: 'ERR_PG_CONNECTION_ENDED' })) process.nextTick(() => { self.emit('end') diff --git a/packages/pg/test/integration/client/connection-timeout-tests.js b/packages/pg/test/integration/client/connection-timeout-tests.js index 3d6b83664..c945cd5ca 100644 --- a/packages/pg/test/integration/client/connection-timeout-tests.js +++ b/packages/pg/test/integration/client/connection-timeout-tests.js @@ -82,6 +82,10 @@ suite.test('expired connection timeout', (done) => { .catch((err) => { assert(err instanceof Error) assert(/timeout expired\s*/.test(err.message)) + // the native client gets this error from libpq, without a code + if (!helper.args.native) { + assert.equal(err.code, 'ERR_PG_CONNECTION_TIMEOUT') + } closeServer(done) }) .then(() => clearTimeout(timeoutId)) diff --git a/packages/pg/test/unit/client/query-timeout-tests.js b/packages/pg/test/unit/client/query-timeout-tests.js index dbf20cf1f..31e763a84 100644 --- a/packages/pg/test/unit/client/query-timeout-tests.js +++ b/packages/pg/test/unit/client/query-timeout-tests.js @@ -13,6 +13,7 @@ test('query timeout with Submittable without callback delivers error via handleE const query = new Query({ text: 'SELECT 1' }) query.handleError = (err) => { assert.equal(err.message, 'Query read timeout') + assert.equal(err.code, 'ERR_PG_QUERY_TIMEOUT') done() } @@ -29,6 +30,7 @@ test('query timeout with Submittable with callback delivers error via callback', client.query(query, (err) => { assert.equal(err.message, 'Query read timeout') + assert.equal(err.code, 'ERR_PG_QUERY_TIMEOUT') done() }) }) diff --git a/packages/pg/test/unit/client/stream-and-query-error-interaction-tests.js b/packages/pg/test/unit/client/stream-and-query-error-interaction-tests.js index 166cd9f35..19054a951 100644 --- a/packages/pg/test/unit/client/stream-and-query-error-interaction-tests.js +++ b/packages/pg/test/unit/client/stream-and-query-error-interaction-tests.js @@ -21,7 +21,8 @@ suite.test('emits end when not in query', function () { client.query( 'SELECT NOW()', assert.calls(function (err, result) { - assert(err) + assert.equal(err.message, 'Connection terminated unexpectedly') + assert.equal(err.code, 'ERR_PG_CONNECTION_TERMINATED') }) ) }) diff --git a/packages/pg/test/unit/connection/error-tests.js b/packages/pg/test/unit/connection/error-tests.js index 71ca36566..bc93e7d10 100644 --- a/packages/pg/test/unit/connection/error-tests.js +++ b/packages/pg/test/unit/connection/error-tests.js @@ -65,18 +65,21 @@ const SSLNegotiationPacketTests = [ { testName: 'connection does not emit ECONNRESET errors during disconnect also when using SSL', errorMessage: null, + errorCode: null, response: 'S', responseType: 'sslconnect', }, { testName: 'connection emits an error when SSL is not supported', errorMessage: 'The server does not support SSL connections', + errorCode: 'ERR_PG_SSL_NOT_SUPPORTED', response: 'N', responseType: 'error', }, { testName: 'connection emits an error when postmaster responds to SSL negotiation packet', errorMessage: 'There was an error establishing an SSL connection', + errorCode: 'ERR_PG_SSL_NEGOTIATION_FAILED', response: 'E', responseType: 'error', }, @@ -170,6 +173,7 @@ for (const tc of SSLNegotiationPacketTests) { assert.emits(con, tc.responseType, function (err) { if (tc.errorMessage !== null || err) { assert.equal(err.message, tc.errorMessage) + assert.equal(err.code, tc.errorCode) } con.end() socket.destroy()