Skip to content

fix(query-core): ignore a retained thenable callback invoked after settling - #11128

Open
sobol-sudo wants to merge 3 commits into
TanStack:mainfrom
sobol-sudo:test/query-core-thenable
Open

fix(query-core): ignore a retained thenable callback invoked after settling#11128
sobol-sudo wants to merge 3 commits into
TanStack:mainfrom
sobol-sudo:test/query-core-thenable

Conversation

@sobol-sudo

@sobol-sudo sobol-sudo commented Jul 28, 2026

Copy link
Copy Markdown

thenable.ts is used by hydration, retryer and queryObserver, but it has no test file of its own, so its behaviour is only covered indirectly. This adds a unit suite for both exports, and fixes a defect the suite surfaced.

Fix

pendingThenable deletes resolve/reject on settle, with the comment that this avoids calling them twice. A caller that kept a reference to one of them beforehand bypasses that guard, and finalize then applies to an already settled promise:

const thenable = pendingThenable<string>()
const retainedReject = thenable.reject

thenable.resolve('data')
retainedReject(new Error('late'))

// before: status 'rejected', value 'data', reason Error: late — while the promise resolves to 'data'
// after:  status 'fulfilled', value 'data', no reason

The underlying promise correctly ignores the late call, so only the advertised state was wrong. That matters because React reads .status/.value/.reason to unwrap promises optimistically, so a thenable claiming rejected while actually fulfilled can surface an error that never happened. finalize now returns early once the thenable is no longer pending.

Tests

pendingThenable — starts out pending with resolve/reject attached; exposes value/reason and drops the pending props once settled; ignores a retained callback invoked after settling; does not surface an unhandled rejection when nobody awaits it.

tryResolveSync — returns undefined for a native promise, including an already resolved one, since then always defers to a microtask; returns the data for a thenable that calls back synchronously; supports a synchronous thenable whose then() returns something without a catch method, which is the case the ?.catch guard exists for; returns undefined when a synchronous thenable resolves with undefined; returns undefined for a rejected promise without leaving the rejection unhandled.

The retained-callback test fails on main with expected 'rejected' to be 'fulfilled' and passes with the change. Locally query-core is 565 passed / 25 files, no type errors, ESLint clean.

Patch changeset included since this changes published behaviour.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed thenable behavior so a retained settlement callback can’t overwrite the thenable after it has already settled.
  • Tests
    • Added Vitest coverage for thenable pending/settled state transitions, including value/reason exposure and prevention of repeated settlement.
    • Added coverage for synchronous thenable resolution behavior and handling of rejected/undefined sync outcomes.
    • Confirmed rejections don’t trigger unhandled rejection events after the task queue drains.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Updates pendingThenable to ignore settlement callbacks invoked after settlement, adds a patch changeset, and introduces Vitest coverage for thenable state transitions, synchronous resolution, undefined values, and rejected promises.

Changes

Thenable settlement protection and validation

Layer / File(s) Summary
Guard settled thenables
packages/query-core/src/thenable.ts, .changeset/olive-cows-repeat.md
finalize now returns early when the thenable is no longer pending; the changeset documents the patch.
Validate thenable utilities
packages/query-core/src/__tests__/thenable.test.tsx
Tests settlement states, callback cleanup, ignored late callbacks, synchronous resolution behavior, undefined values, and suppression of unhandled rejection events.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly matches the main change: fixing retained thenable callbacks after settlement in query-core.
Description check ✅ Passed The description covers the change, motivation, tests, and release impact, though it does not follow the template headings or checklist format exactly.
✨ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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/__tests__/thenable.test.tsx`:
- Around line 39-46: Update the test around pendingThenable to retain the reject
callback before calling resolve, invoke that stale callback afterward, and
assert the thenable still resolves with the original value. Then update the
settlement/finalization logic in pendingThenable so callbacks captured before
finalization cannot mutate status or reason, and repeated finalization remains
idempotent.
🪄 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 Plus

Run ID: 3f538398-6e28-4e86-b535-72a8fb454acd

📥 Commits

Reviewing files that changed from the base of the PR and between 75b2d10 and 306a58a.

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

Comment thread packages/query-core/src/__tests__/thenable.test.tsx
@sobol-sudo sobol-sudo changed the title test(query-core): add unit tests for thenable fix(query-core): ignore a retained thenable callback invoked after settling Jul 29, 2026
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