Skip to content

fix(signals): surface sync rejection from thenables (closes #2764)#2783

Open
tsushanth wants to merge 2 commits into
solidjs:nextfrom
tsushanth:fix-sync-thenable-rejection
Open

fix(signals): surface sync rejection from thenables (closes #2764)#2783
tsushanth wants to merge 2 commits into
solidjs:nextfrom
tsushanth:fix-sync-thenable-rejection

Conversation

@tsushanth

Copy link
Copy Markdown

Closes #2764.

When a thenable invokes its rejection handler synchronously during .then(...), the error was being silently dropped. The handler's if (!isSync) handleError(e) branch skipped the call during the sync window, and the subsequent if (!resolved) branch unconditionally threw NotReadyError — so the boundary observed pending forever and never reached Errored.

The fix mirrors the success handler's sync/async split for rejections: capture the error inside the sync window, then after .then returns, surface it through handleError and rethrow so the calling computation propagates it to the nearest error boundary.

Test

Added a regression test in tests/syncThenable.test.ts that wraps a synchronously-rejecting thenable in a createMemo under createErrorBoundary and asserts the boundary receives the error.

Without the fix: test times out / boundary stays pending.
With the fix: boundary catches the error.

 solidjs#2766)

When `onSettled` is called from a children-forbidden scope (most commonly
from inside another `onSettled` callback, which runs as a tracked
effect), the implementation fell back to enqueueing a one-shot callback
on the global queue and invoked the returned cleanup eagerly inside the
same flush:

  globalQueue.enqueue(EFFECT_USER, () => {
    const cleanup = callback();
    …
    cleanup?.();
  });

That immediately tore down helpers whose intent was "install on settle,
tear down on dispose" — composable helpers using `onSettled` internally
saw their subscription set up and ripped down in the same tick (solidjs#2766).

Capture the calling owner and, when the callback returns a function,
attach it via `cleanup()` under that owner with `runWithOwner`. The
cleanup now fires when the captured owner is disposed, matching the
contract of the non-forbidden path. Orphaned helpers (no owner at all)
drop the cleanup rather than run it immediately — there is no point at
which firing it would be correct.

Add a regression test in `tests/onSettled.test.ts` that wraps the
reporter's pattern (a nested `onSettled` inside an outer `onSettled`)
and asserts the subscription stays alive until `dispose()`.
@changeset-bot

changeset-bot Bot commented Jun 13, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 32ba5bf

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

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