Skip to content

fix(thenable): use Object.create() to avoid Promise mutation in headless Chromium#10653

Open
Zelys-DFKH wants to merge 2 commits intoTanStack:mainfrom
Zelys-DFKH:fix/promise-mutation-headless-chromium-10509
Open

fix(thenable): use Object.create() to avoid Promise mutation in headless Chromium#10653
Zelys-DFKH wants to merge 2 commits intoTanStack:mainfrom
Zelys-DFKH:fix/promise-mutation-headless-chromium-10509

Conversation

@Zelys-DFKH
Copy link
Copy Markdown
Contributor

@Zelys-DFKH Zelys-DFKH commented May 7, 2026

Your issue nailed it. Headless Chromium treats Promise internal slots as sealed, so mutations like thenable.status = 'pending' silently fail. The .then() handler fires, but the wrapper never updates — notification callbacks never run, and useQuery stays pending forever.

It's a small but real problem for testing. I've spent hours debugging similar patterns.

The Fix

Create a wrapper object with the Promise as its prototype instead of mutating the Promise directly. This lets custom properties live on the wrapper while preserving all Promise semantics:

const promise = new Promise<T>((_resolve, _reject) => {
  resolve = _resolve
  reject = _reject
})

promise.catch(() => {
  // noop
})

const thenable = Object.create(promise) as PendingThenable<T>
thenable.status = 'pending'

The wrapper's custom properties work. The .then() chain works via prototype. No behavior changes, fully backward compatible.

Files Changed

  • packages/query-core/src/thenable.ts — Wrap the Promise instead of mutating it

Testing

Verify in headless mode:

const { result } = renderHook(() => useQuery({
  queryKey: ['test'],
  queryFn: async () => 'data'
}))

// Before fix: result.current.status === 'pending' forever
// After fix: result.current.status === 'success'

Fixes #10509

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an issue where queries could remain pending indefinitely in headless browser environments (e.g., Puppeteer, Playwright), restoring reliable query resolution.
    • Reduced noisy/unhandled Promise rejection warnings in stricter Chromium environments to improve test and CI stability.

…ess Chromium

Resolves TanStack#10509 where useQuery stays pending indefinitely in Puppeteer/Playwright.

Headless Chromium enforces stricter Promise semantics, treating internal slots as sealed.
The custom properties set on the Promise (status, resolve, reject) were silently failing
in these environments. By wrapping the Promise with Object.create() instead of mutating
it directly, we preserve all Promise behavior via the prototype chain while allowing
custom properties to live on the wrapper object.

This maintains full backward compatibility - all consumers call .then() which continues
to work via prototype chain inheritance, and the notification callbacks now fire correctly
in all JavaScript environments.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 73c2d49d-d848-4dfd-9a44-cde9f7349432

📥 Commits

Reviewing files that changed from the base of the PR and between 2110a49 and 1c40c3f.

📒 Files selected for processing (1)
  • packages/query-core/src/thenable.ts

📝 Walkthrough

Walkthrough

This PR refactors pendingThenable to derive the exported thenable from an internal Promise via Object.create(), suppresses unhandled rejection noise with a no-op .catch(), and adds a changeset entry documenting the fix for headless Chromium Promise mutation.

Changes

Thenable Promise Mutation Fix

Layer / File(s) Summary
Core Thenable Refactoring
packages/query-core/src/thenable.ts
pendingThenable() now creates an internal Promise, attaches a no-op .catch(), then returns a thenable created via Object.create(promise) with .status = 'pending'. Existing settlement logic still assigns fulfilled/rejected values and removes resolve/reject.
Release Notes
.changeset/fix-promise-mutation-10509.md
Adds a changeset patch entry documenting the Object.create()-based thenable fix to avoid Promise mutation in headless Chromium.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐰 A rabbit with whiskers and keys to create,

Wrapped a Promise in shelter, avoiding fate.
No mutation to bother, no pending delay,
The query bounds forward and nibbles away.
Hooray for Object.create() — hoppity hooray!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: using Object.create() to fix Promise mutation issues in headless Chromium, matching the core modification in thenable.ts.
Description check ✅ Passed The description provides detailed context about the problem, explains the fix with code examples, identifies files changed, and includes testing instructions. However, the checklist items for contributing guide compliance and local test execution are not explicitly checked off.
Linked Issues check ✅ Passed The PR directly addresses issue #10509 by implementing the Object.create(promise) wrapper pattern to prevent Promise mutation in headless Chromium, which resolves the core complaint that useQuery remains pending indefinitely despite queryFn completing successfully.
Out of Scope Changes check ✅ Passed All changes (thenable.ts and changeset file) are directly scoped to fixing the Promise mutation issue in headless Chromium. No extraneous or unrelated modifications are present.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 7, 2026

🤖 Nx Cloud AI Fix Eligible

An automatically generated fix could have helped fix failing tasks for this run, but Self-healing CI is disabled for this workspace. Visit workspace settings to enable it and get automatic fixes in future runs.

To disable these notifications, a workspace admin can disable them in workspace settings.


View your CI Pipeline Execution ↗ for commit 2110a49

Command Status Duration Result
nx run-many --target=build --exclude=examples/*... ❌ Failed 4s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-07 23:10:22 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 7, 2026

More templates

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@10653

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@10653

@tanstack/preact-query

npm i https://pkg.pr.new/@tanstack/preact-query@10653

@tanstack/preact-query-devtools

npm i https://pkg.pr.new/@tanstack/preact-query-devtools@10653

@tanstack/preact-query-persist-client

npm i https://pkg.pr.new/@tanstack/preact-query-persist-client@10653

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@10653

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@10653

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@10653

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@10653

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@10653

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@10653

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@10653

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@10653

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@10653

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@10653

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@10653

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@10653

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@10653

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@10653

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@10653

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@10653

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@10653

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@10653

commit: 2110a49

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/query-core/src/thenable.ts`:
- Around line 57-59: Creating thenable via Object.create(promise) yields an
object missing Promise internal slots so React's Suspense/use will throw when
calling then; fix by copying/binding the Promise prototype methods from the
underlying promise onto the PendingThenable instance (e.g., set thenable.then =
promise.then.bind(promise), thenable.catch = promise.catch.bind(promise),
thenable.finally = promise.finally.bind(promise)) so that thenable (returned as
nextResult.promise) behaves like a real Promise while still carrying the extra
status/result fields.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 380bf36f-7464-4be4-ab48-a6ff0f738d58

📥 Commits

Reviewing files that changed from the base of the PR and between 9d1ce70 and 2110a49.

📒 Files selected for processing (2)
  • .changeset/fix-promise-mutation-10509.md
  • packages/query-core/src/thenable.ts

Comment thread packages/query-core/src/thenable.ts
…n React Suspense

The Object.create(promise) wrapper pattern requires explicit method binding.
Without it, React's Suspense and other code calling .then() fails with
'Method Promise.prototype.then called on incompatible receiver' because
the native Promise implementation requires internal slots [[PromiseState]]
and [[PromiseResult]] to exist on the object .then() is called on.

Binding (e.g., thenable.then = promise.then.bind(promise)) ensures the
methods execute in the correct context (the underlying promise with slots)
while preserving all custom wrapper properties (status, resolve, reject).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useQuery stays pending in headless Chromium despite queryFn resolving successfully

1 participant