CloudSQLInstance.close() calls socket.destroy(err) after connection pool drain, causing unhandled 'error' event and process crash
Environment
@google-cloud/cloud-sql-connector: v1.11.0
- Node.js: 22.x
- Database driver:
pg (node-postgres)
Description
When connector.close() is called after a pg connection pool has been fully drained via pool.end(), CloudSQLInstance.close() calls socket.destroy(new CloudSQLConnectorError({ code: 'ERRCLOSED', ... })) on each tracked socket. This triggers an 'error' event emission via process.nextTick. By that point, the pg driver has already removed its error listeners from the sockets as part of the pool shutdown, so the 'error' event fires with no listener — causing an uncaught exception that crashes the process.
Steps to Reproduce
- Establish a Cloud SQL connection using the connector
- Run a query through a
pg.Pool
- Call
await pool.end() to drain the pool
- Call
connector.close() immediately after
Observed Behavior
CloudSQLConnectorError: The connector was closed.
at CloudSQLInstance.close (cloud-sql-instance.js:259:32)
at Connector.close (connector.js:252:32)
...
This error surfaces as an unhandled 'error' event on the socket (scheduled via process.nextTick inside socket.destroy(err)). In Node.js 15+, unhandled errors escalate to uncaughtException, causing the process to crash.
Expected Behavior
Calling connector.close() after pool.end() should not crash the process. Since the pool has already been drained and all connections cleanly terminated, there are no pending operations on the sockets. Destroying them as part of connector cleanup is correct, but emitting an error event to signal "The connector was closed" is unnecessary and harmful at this point.
Root Cause
In cloud-sql-instance.js, close() iterates over tracked sockets and calls:
socket.destroy(new errors_1.CloudSQLConnectorError({
code: 'ERRCLOSED',
message: 'The connector was closed.',
}));
socket.destroy(err) in Node.js schedules an 'error' event via process.nextTick. After pool.end() completes, the pg driver has already torn down its clients and detached error listeners from the underlying sockets. When the scheduled 'error' event fires in the next tick, no listener is present, resulting in an uncaught exception.
Suggested Fix
Call socket.destroy() without an error argument when close() is invoked intentionally, since passing an error is only meaningful when there are pending operations that need to be notified of an abnormal termination:
- socket.destroy(new errors_1.CloudSQLConnectorError({
- code: 'ERRCLOSED',
- message: 'The connector was closed.',
- }));
+ socket.destroy();
Alternatively, ensure the sockets have a no-op 'error' listener attached before calling destroy(err):
socket.once('error', () => {}); // prevent unhandled 'error' event
socket.destroy(new errors_1.CloudSQLConnectorError({ ... }));
Context
This was encountered when using firebase-tools v15.18.0, which calls connector.close() (without await) inside a cleanup function after pool.end() in connect.js. The race between the microtask queue resolving connector.close()'s internal operations and the already-drained pool's socket teardown triggers the issue consistently in CI environments running Node.js 22.
Workaround
Patching cloud-sql-instance.js to remove the error argument from socket.destroy() resolves the crash.
CloudSQLInstance.close()callssocket.destroy(err)after connection pool drain, causing unhandled 'error' event and process crashEnvironment
@google-cloud/cloud-sql-connector: v1.11.0pg(node-postgres)Description
When
connector.close()is called after apgconnection pool has been fully drained viapool.end(),CloudSQLInstance.close()callssocket.destroy(new CloudSQLConnectorError({ code: 'ERRCLOSED', ... }))on each tracked socket. This triggers an'error'event emission viaprocess.nextTick. By that point, thepgdriver has already removed its error listeners from the sockets as part of the pool shutdown, so the'error'event fires with no listener — causing an uncaught exception that crashes the process.Steps to Reproduce
pg.Poolawait pool.end()to drain the poolconnector.close()immediately afterObserved Behavior
This error surfaces as an unhandled
'error'event on the socket (scheduled viaprocess.nextTickinsidesocket.destroy(err)). In Node.js 15+, unhandled errors escalate touncaughtException, causing the process to crash.Expected Behavior
Calling
connector.close()afterpool.end()should not crash the process. Since the pool has already been drained and all connections cleanly terminated, there are no pending operations on the sockets. Destroying them as part of connector cleanup is correct, but emitting an error event to signal "The connector was closed" is unnecessary and harmful at this point.Root Cause
In
cloud-sql-instance.js,close()iterates over tracked sockets and calls:socket.destroy(err)in Node.js schedules an 'error' event via process.nextTick. Afterpool.end()completes, thepgdriver has already torn down its clients and detached error listeners from the underlying sockets. When the scheduled 'error' event fires in the next tick, no listener is present, resulting in an uncaught exception.Suggested Fix
Call
socket.destroy()without an error argument whenclose()is invoked intentionally, since passing an error is only meaningful when there are pending operations that need to be notified of an abnormal termination:Alternatively, ensure the sockets have a no-op 'error' listener attached before calling
destroy(err):Context
This was encountered when using firebase-tools v15.18.0, which calls connector.close() (without await) inside a cleanup function after pool.end() in connect.js. The race between the microtask queue resolving connector.close()'s internal operations and the already-drained pool's socket teardown triggers the issue consistently in CI environments running Node.js 22.
Workaround
Patching
cloud-sql-instance.jsto remove the error argument fromsocket.destroy()resolves the crash.