db-postgres: connect() rejects its `initializing` promise with no argument, producing an unhandledRejection with reason `undefined`
Maintainers usually reply within 1 day
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 76/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- node.js, postgresql, typescript
Research direction
Read packages/db-postgres/src/index.ts and packages/db-postgres/src/connect.ts, then check the rejectInitializing type in packages/drizzle/src/types.ts and the consumer in packages/drizzle/src/transactions/beginTransaction.ts. The issue describes two possible fixes; trace how connection failures reach the promise and how beginTransaction handles rejection. Done means the orphan rejection no longer reports undefined while connection failures still surface.
Written by the indexing model from the issue text.
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: false
Patching 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:
[email protected]can only reject its connect promise with a truthy value —err ? rej(err) : res(client).[email protected]'sClient.end()with no callback returns a promise with no reject path at all.@opentelemetry/[email protected]'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)
- Dominant language
- TypeScript
- Stars
- 45.3k
- Forks
- 4.3k
- Avg merge
- 1d 21h
- Merged PRs (30d)
- 272
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from payloadcms/payload
-
bug: Local API call with req resets req.fallbackLocale to defaultLocalePossibly taken A pull request linked to this issue is open or already merged. Openarea: core Bug created-by: Contributor
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
payloadcms/payload#18768 ·
Maintainers usually reply within 1 day
-
[plugin-multi-tenant] Infinite redirect loop with custom tenantFieldName and isGlobal collectionsPossibly taken A pull request linked to this issue is open or already merged. OpenBug plugin: multi-tenant status: needs-triage v3
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
payloadcms/payload#18690 ·
Maintainers usually reply within 1 day
-
Global update drops overrideAccess, so Local API updateGlobal fails filterOptions validation (3.90 regression)Possibly taken @PINYOPATTANAWASANPORN claimed this 8 days ago. Openarea: core Bug
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
payloadcms/payload#18674 · 1 comment ·
Maintainers usually reply within 1 day
-
plugin-cloud-storage: unused `find-node-modules` dependency pulls in `braces` (GHSA-vfj7-8cjw-p6xm, no patched version)Possibly taken @MokiMeow claimed this 12 days ago. OpenBug plugin: cloud-storage plugin: storage-* status: needs-triage v3
Difficulty 1/5 Under an hour Newbie friendliness 75/100
payloadcms/payload#18623 ·
Maintainers usually reply within 1 day
-
createImageSizes decodes the source once per image size, all at once, so one upload can take hundreds of MBPossibly taken @Kshot3000 claimed this 15 days ago. Openarea: core Bug Uploads
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
payloadcms/payload#18531 · 1 reaction ·
Maintainers usually reply within 1 day
All issues in payloadcms/payload
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
endora-commerce/endora-commerce#397 ·
Maintainers usually reply within 1 day
-
level/task
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
wazuh/wazuh-dashboard-plugins#9472 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 63/100
Maintainers usually reply within 1 day
-
ready-for-agent
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
xavxyz/bei-shan-lin-shui#57 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
opennextjs/opennextjs-cloudflare#1461 ·
Maintainers usually reply within 1 day