porsager / porsager/postgres

fetchArrayTypes() promise is discarded in ReadyForQuery: a failing fetch_types query crashes the process via unhandled rejection

Open Beginner friendly
#1,192 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
8.7k
Forks
374
Avg merge
11d 16h
Merged PRs (30d)
1

Description

The defect

ReadyForQuery in src/connection.js calls the async fetchArrayTypes() and discards its promise:

if (needsTypes) {
  initial.reserve && (initial = null)
  return fetchArrayTypes()
}

fetchArrayTypes() awaits the internal pg_type catalog query. That query runs as the first statement on every new connection (with the default fetch_types: true). If it fails, the awaited Query rejects, the async function's promise rejects, and nothing ever subscribed to it, so the raw PostgresError surfaces as an unhandledRejection. Runtimes that exit on unhandled rejections kill the process (we observed Node.js process exited with exit status: 128 on Vercel, once per fresh connection during a database incident). The user's own query is unaffected by the discard: it executes afterwards and fails or succeeds on its own terms, so the crash is not catchable from user code.

This is the same signature as #279 (2022), which was closed as not reproducible at the time. It reproduces deterministically now.

Minimal reproduction (postgres 3.4.9, Node 22, plain Postgres 17)

A 1ms statement_timeout bound at the startup handshake reliably cancels the internal catalog query before it completes:

import postgres from 'postgres'

process.on('unhandledRejection', (e) => console.log('UNHANDLED', e.code, e.message))

const sql = postgres(process.env.DATABASE_URL, {
  prepare: false,
  connection: { statement_timeout: 1 },
})

try {
  await sql`select pg_sleep(2)`
} catch (e) {
  console.log('caught', e.code)
}

Output:

caught 57014
UNHANDLED 57014 canceling statement due to statement timeout

Any other failure of the types query produces the same shape; statement_timeout is just the deterministic trigger. In production the trigger was a pooler incident where fresh backends cancelled their first statements.

Suggested fix

Observe the promise at the call site. This preserves the current semantics exactly: on a types-fetch failure the connection already continues without array types and already executes the pending initial query, so the only change is that the rejection no longer escapes:

if (needsTypes) {
  initial.reserve && (initial = null)
  return fetchArrayTypes().catch(() => { /* connection proceeds without array types */ })
}

Verified present in 3.4.9 and on master as of 2026-08-13 (same shape in src, cjs/src, and cf/src). We are running exactly this one-line change as a patch in production. Happy to open a PR if useful.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at ReadyForQuery in src/connection.js and compare the corresponding paths in cjs/src and cf/src; run the minimal Node 22/Postgres reproduction with a 1ms statement timeout. Done means the cancellation remains catchable by the user's query and no unhandledRejection is emitted from the internal types query.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js, postgresql
Domain
database
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.