Repository navigation
db-postgres: connect() rejects its initializing promise with no argument, producing an unhandledRejection with reason undefined #18656
Description
Describe the Bug
@payloadcms/db-postgres rejects its internal initializing promise with no argument when connect() fails. Because the only thing that ever awaits that promise is beginTransaction, a failed connect on a request path that opens no transaction leaves the rejection unhandled — and since the reason is undefined, Node attaches no stack to it.
The result is an unhandledRejection with reason === undefined. In an app with Sentry (or any unhandledRejection reporter) wired up, that arrives as an event with no message, no stack and no in-app frames, pointing only at the reporter's own handler. It is effectively undebuggable from the report: nothing in it names Payload, Postgres, or the connection at all.
The two lines, at v3.90.2:
-
packages/db-postgres/src/index.ts#L80-L82— the promise is created and no handler is attached:const initializing = new Promise<void>((res, rej) => { resolveInitializing = res rejectInitializing = rej })
-
packages/db-postgres/src/connect.ts#L109-L111— it is rejected with nothing, and then a separate realErroris thrown:if (typeof this.rejectInitializing === 'function') { this.rejectInitializing() // ← no argument ⇒ rejects with `undefined` } throw new Error(`Error: cannot connect to Postgres: ${err.message}`)
Still present on main (packages/db-postgres/src/connect.ts, this.rejectInitializing() immediately before the throw). The type itself guarantees it can never carry a reason — rejectInitializing: () => void, declared with no parameter, in packages/drizzle/src/types.ts and the db-postgres types.
The only consumer is packages/drizzle/src/transactions/beginTransaction.ts (await this.initializing). packages/drizzle/src/destroy.ts replaces the promise rather than awaiting it. So on any read path that does not begin a transaction, nothing is attached when the rejection lands.
Two suggested fixes, either of which is sufficient:
- Pass the error:
this.rejectInitializing(err), and widen the type to(err?: unknown) => void. The rejection then at least carries a stack for whoever is listening. - Attach a no-op handler where the promise is created, so the orphan can never surface:
initializing.catch(() => {}).connect()already throws the real error separately, andbeginTransaction's ownawaitstill rejects, so nothing is swallowed.
(1) is the more useful of the two; doing both costs nothing.
Link to the code that reproduces this issue
https://github.com/payloadcms/payload/blob/v3.90.2/packages/db-postgres/src/connect.ts#L109-L111
I have not pushed a create-payload-app scaffold for this one, because the defect is visible in the published source above and does not depend on app config: rejectInitializing is typed () => void, it is called with no argument, and the promise it rejects has no handler attached at creation. Happy to put a scaffold together if you'd rather have one to run — say so and I will.
Reproduction Steps
Any postgresAdapter app where connect() fails will do it. Forcing it deterministically:
-
Build the app and start it (
output: standalonein my case, but plainnext startbehaves the same). -
Set
connectionTimeoutMillison the adapter'spoollow enough that the pool's timer expires during the first server render, which blocks the event loop while the CMS config is built. WithconnectionTimeoutMillis: 1through10it fires every time:db: postgresAdapter({ pool: { connectionString: process.env.DATABASE_URL, connectionTimeoutMillis: 1 }, })
-
Request any page that reads from the database but opens no transaction (a plain
payload.find()in a server component is enough). -
Observe two things in the output: Payload's own
Error: cannot connect to Postgres: Connection terminated due to connection timeout— correct and useful — and, separately, anunhandledRejectionwhose reason isundefined.
With a listener installed to inspect it:
process.on('unhandledRejection', (reason) => {
console.log('typeof reason:', typeof reason, '| isError:', reason instanceof Error)
})
// → typeof reason: undefined | isError: falsePatching the reject capability of new Promise(executor) to log a stack when called with a non-Error names the call site directly:
!! new Promise(…) reject() with a non-Error reason: undefined
at Object.rejectInitializing (…)
at Object.i [as connect] (…/connect.js)
at async …init (…)
Ruled out below Payload, by reading the installed source, so that the report lands in the right place:
pg-pool@3.13.0can only reject its connect promise with a truthy value —err ? rej(err) : res(client).pg@8.20.0'sClient.end()with no callback returns a promise with no reject path at all.@opentelemetry/instrumentation-pg@0.51.1'shandleConnectResultre-rejects the identical error.
Which area(s) are affected?
area: db-postgres
Environment Info
payload: 3.90.2
@payloadcms/db-postgres: 3.90.2
@payloadcms/drizzle: 3.90.2
pg: 8.20.0
pg-pool: 3.13.0
next: 15.4.11
node: 22.16.0 (also reproduced on 22.23.1)
Activity
Metehan-Bicer commentedon Oct 10, 2026
Hi! I'd like to take this. On
mainthe same no-argumentrejectInitializing()call is in theconnect.tsof db-postgres, db-sqlite, db-vercel-postgres and db-d1-sqlite, anddrizzle/src/destroy.tscreates a freshinitializingthe same way. I'd apply both of your suggestions to all of them: pass the connect error and widen the type to(err?: unknown) => void, and attach a no-op handler where the promise is created. I'll add a unit test with an adapter whose connect fails.