Skip to content

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

Open
Zelys-DFKH wants to merge 2 commits into
TanStack: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 into
TanStack: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, improving test and CI stability.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fb77b488-4c1f-4251-8570-9df51766ff79

📥 Commits

Reviewing files that changed from the base of the PR and between 1c40c3f and 8cecf55.

📒 Files selected for processing (2)
  • .changeset/fix-promise-mutation-10509.md
  • packages/query-core/src/thenable.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/fix-promise-mutation-10509.md

📝 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'. It proxies .then/.catch/.finally to the internal promise and preserves the existing settlement behavior.
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 found a promise in the hay,

Wrapped it gentle, so it wouldn't stray.
No mutated hops, no endless wait,
The query awoke and danced with fate.
Hooray for sheltering Object.create() today!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(thenable): use Object.create() to avoid Promise mutation in headless Chromium' clearly and specifically describes the main fix applied to resolve issue #10509.
Description check ✅ Passed The description comprehensively explains the problem, the fix approach, implementation details, testing verification, and includes a changeset entry. It follows the template structure with clear problem/solution sections.
Linked Issues check ✅ Passed The PR implementation directly addresses the core requirement from #10509: using Object.create() to wrap the Promise and prevent direct mutations, allowing useQuery to properly transition to success in headless Chromium environments.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the Promise mutation issue in headless Chromium. The changeset file and thenable.ts modifications are both focused on the single objective of issue #10509.

✏️ 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
Zelys added 2 commits May 21, 2026 15:20
…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.
…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).
@Zelys-DFKH Zelys-DFKH force-pushed the fix/promise-mutation-headless-chromium-10509 branch from 1c40c3f to 8cecf55 Compare May 21, 2026 20:24
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