node-postgres (pg) app on PostgreSQL dies outright after database restart instead of failing queries
node-postgres (pg) app on PostgreSQL: after a database restart the Node process dies outright instead of just failing queries. Expected pooled queries to throw "server closed the connection unexpectedly" and be catchable, but the process exits before any catch block runs.
Added the pool recycling options that seemed like the obvious fix - idleTimeoutMillis, maxLifetimeSeconds, connectionTimeoutMillis, keepAlive: true - and the process still dies on restart. Wrapping every pool.query() call site in try/catch also does not help; the crash happens with no query in flight, and the stack trace points at an EventEmitter rather than at application code. Reducing max and lowering idleTimeoutMillis narrows the window but does not close it.
Root cause
pg's Pool emits an 'error' event on idle clients, not just on the client you checked out. When the server restarts it closes every pooled connection at once, so the pool emits 'error' for each idle client with no query in flight.
On a Node EventEmitter, an 'error' event with no registered listener is re-thrown as an uncaught exception and terminates the process. Nothing at a pool.query() call site can catch it, because the emission is not associated with any pending query - which is why per-call try/catch and the pool timeout options have no effect on the crash.
Fix
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
pool.on("error", (err) => {
// Pool already destroyed this client by the time this fires.
// Log only - do not rethrow.
console.error("[db] idle client error (connection discarded):", err.message);
});The pool removes the failed client before emitting, so the handler must not rethrow or attempt reconnection - doing so reintroduces the crash.
Recycling options are still needed, for a different problem
The error handler stops the crash. It does NOT stop stale connections from being handed to callers. Two separate failures, which is why fixing only one leaves the symptom apparently unchanged:
new Pool({
idleTimeoutMillis: 30_000,
maxLifetimeSeconds: 1_800, // requires pg >= 8.11; older versions ignore it silently
connectionTimeoutMillis: 5_000,
keepAlive: true,
keepAliveInitialDelayMillis: 10_000,
});maxLifetimeSeconds landed in pg 8.11 (pg-pool 3.6). On older pg it is accepted and silently ignored at runtime - no error, no effect - so it is easy to believe it is working when it is not. An older @types/pg rejects the property at compile time, which is the only signal you get.
Retrying the stale checkout
After a restart the entire pool is stale, so a single retry often just draws another dead connection. Retry in a small loop with backoff; each failed attempt makes pg destroy that client, so the pool drains itself as the loop runs. Match on SQLSTATE 57P01 (admin_shutdown), 57P02, 57P03, 08006, plus socket codes ECONNRESET/EPIPE.
Caveat: a connection can also drop mid-statement, so a retry may re-execute work that already committed. Safe for reads and idempotent writes; for non-idempotent writes check out a client explicitly and use a transaction, where the drop aborts server-side and no retry should occur.