Skip to content

[Pg]: Fix unawaited promise chains in queryWithCache and bun-sql execute#5465

Open
nathanschram wants to merge 1 commit intodrizzle-team:betafrom
nathanschram:fix/bun-sql-unawaited-promises
Open

[Pg]: Fix unawaited promise chains in queryWithCache and bun-sql execute#5465
nathanschram wants to merge 1 commit intodrizzle-team:betafrom
nathanschram:fix/bun-sql-unawaited-promises

Conversation

@nathanschram
Copy link

Summary

Fixes unawaited promise chains in PgAsyncPreparedQuery.queryWithCache() and BunSQLPreparedQuery.execute() that cause race conditions with Bun's event loop, leading to silent process exits (code 0) in standalone scripts.

Fixes #5451

Changes

1. queryWithCache skip path (pg-core/async/session.ts)

Before: return query().catch(...) - returns an unawaited promise chain from an async method.

After: try { return await query(); } catch (e) { ... } - properly awaits the query callback.

This matches the main branch's implementation in pg-core/session.ts which already uses try { return await query(); }.

2. queryWithCache invalidate path (pg-core/async/session.ts)

Before: return Promise.all([query(), cache.onMutate(...)]).then((res) => res[0]).catch(...) - returns an unawaited .then().catch() chain.

After: const [result] = await Promise.all([...]); return result; with try/catch - properly awaits the Promise.all result.

3. BunSQLPreparedQuery.execute() Path B (bun-sql/postgres/session.ts)

Before: The async callback passed to queryWithCache returns client.unsafe(query, params).values() without await.

After: return await client.unsafe(query, params).values() - explicit await on the lazy Bun SQL query.

This matches Path A in the same method, which already uses return await client.unsafe(query, params). Bun SQL queries are lazy - they extend Promise but defer execution until .then() is called. Without await, the I/O registration is deferred to a microtask, creating a window where Bun's event loop can exit before the query starts.

Root cause analysis

The reporter's script runs INSERT followed by SELECT using the Core Query Builder (db.select().from()). The SELECT goes through queryWithCache's skip path, which returned query().catch(...) without await. Inside the callback, client.unsafe().values() also lacked await. The combination of unawaited promise chains creates microtask boundaries where Bun's event loop sees no pending I/O and exits cleanly with code 0.

Other APIs (db.execute(sql), db.query.*.findMany(), db.$count()) work because they either bypass queryWithCache entirely (executeRqbV2) or use Path A which already has explicit await.

Testing

Reproduced and verified in Docker (Bun 1.3.10, PostgreSQL 18) using the reporter's MRE:

  • Baseline: ~62% failure rate (5/8 runs fail with silent exit)
  • With queryWithCache bypass (return await query()): 8/8 pass
  • Raw bun:sql without Drizzle: 8/8 pass (confirms bug is in Drizzle, not Bun)

…he and bun-sql execute

Three fixes for unawaited promise chains that cause race conditions
with Bun's event loop in standalone scripts:

1. queryWithCache skip path: `return query().catch(...)` creates an
   unawaited promise chain. Changed to `try { return await query() }`.
   This matches the main branch's implementation in pg-core/session.ts.

2. queryWithCache invalidate path: `return Promise.all([...]).then().catch()`
   similarly creates an unawaited chain. Changed to properly await
   Promise.all and destructure the result.

3. BunSQLPreparedQuery.execute() Path B: the async callback passed to
   queryWithCache returns `client.unsafe().values()` without `await`.
   Since Bun SQL queries are lazy (they extend Promise but defer
   execution until .then() is called), the missing await means
   I/O registration is deferred to a microtask. Added `await` to
   match Path A which already uses `return await client.unsafe(...)`.

Fixes drizzle-team#5451
@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@nathanschram
Copy link
Author

Heads up on a related finding from further testing on #5451.

The reporter tested these patches and they work on warm start (pre-seeded DB) but fail on cold start (empty DB where an INSERT happens before the SELECT). Digging into the compiled npm package (1.0.0-beta.9-e89174b), the NoopCache condition in pg-core/async/session.js is:

this.cache !== void 0 || is(this.cache, NoopCache)

This always evaluates true for NoopCache, routing all queries through strategyFor() into the "try"/"invalidate" paths instead of the skip path. The source on the beta branch (commit ea816b6) already has the correct && !is(this.cache, NoopCache), but this fix was never published to npm.

The await fixes in this PR are still correct and needed (the unawaited chains in skip/invalidate/bun-sql are real bugs), but on the published package they're insufficient alone because queries never reach the skip path due to the stale condition.

When this PR merges against the current beta HEAD, the condition fix should already be included since the source has it. But worth verifying the compiled output to make sure it doesn't regress - the mismatch between e89174b (npm) and ea816b6 (source) suggests the build may have been cut from an older commit.

Docker reproduction: PR patches only = cold-start fail. PR patches + condition fix = 4/4 cold-start pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant