Skip to content

fix(signals): publish value and error outcomes consistently - #3886

Draft
GabbeV wants to merge 1 commit into
solidjs:nextfrom
GabbeV:fix/latest-settled-error
Draft

GabbeV wants to merge 1 commit into
solidjs:nextfrom
GabbeV:fix/latest-settled-error

Conversation

@GabbeV

@GabbeV GabbeV commented Oct 8, 2026

Copy link
Copy Markdown

@ryansolid I started investigating a issue with AI and found that error and value results are handled inconsistently. Trying to fix that found additional issues and I think the PR might have grown a bit out of hand in the end. Please feel free to reuse or discard any parts of this PR that makes sense but I think the tests and description might be useful. I could also ask my AI to decompose into issues if you prefer.

The rest of this description is by the AI:

Summary

Successful and failed results do not consistently follow the same publication and recovery rules in the signals core. A derivation can discover either result before reveal, but the result observed by an effect or an ordinary outside read should belong to the selected observable frame. Currently an error can bypass that selection, and recovery can be lost when the successful payload equals the last good value.

The investigation started with a latest() derivation retaining an obsolete success after its source rejects. For example, createMemo(() => latest(source) + 1) can keep returning its old value after every request has settled, even though both source() and latest(source) throw. It also reports that it is no longer pending.

Following the outcome through the package exposed several related problems:

  • Early error publication: a rejection can invoke an effect's error callback while a rendered async sibling still holds the input frame. A successful result waits in the equivalent graph.
  • Outside reads disagree with the published outcome: during held rejection, an ordinary read can throw while the displayed frame still contains the old success. During pending retry or held recovery, the previously published failure can disappear before the successful replacement reveals.
  • Same-value recovery is missed: returning the last good payload can leave an effect displaying its previous error permanently because payload equality suppresses notification of the changed outcome.
  • Other error paths skip their successful counterparts' bookkeeping: existing projection leaf readers can miss synchronous failure/recovery; resolve, until and refresh rejection can bypass the foreign-frame delivery gate; async comparator failure can retain unobserved lazy readers; and a direct write to a failed writable derivation can leave its old error in place. Diagnostic formatting can also replace the original thrown value if coercion throws.

This change stores published failures separately from working computation status and publishes success or failure at the selected frame's commit. Proposed failures still throw during derivation; effect callbacks wait for reveal. isPending includes held outcome transitions in both directions, and recovery is an outcome change even when its successful payload is unchanged. The other paths above reuse the existing status, publication, flight-error and callback machinery.

There is an explicit contract change for loadingValue: a revealed failure ends the initial seed window, so a pending retry retains that failure instead of serving the placeholder again. The last successful payload, including the seed, remains the derivation's prev; errors never become prev values.

Successful payloads remain unboxed. Two error references are added to the lazy node extension; snapshots reuse their captured slot with an error tag. Fulfilled Error objects remain ordinary data, NotReadyError remains pending control flow, and stack capture is unchanged. This does not redefine next's existing proposed-view behavior for imperative reads of latest/optimistic lanes. The outcome audit documents those distinctions.

How did you test this change?

Rebased onto next@d231b991 (including #3882 and #3885), then rebuilt and reran:

  • Signals suite: 5,145 passed, 28 expected failures, 2 skipped, across 290 files (cd packages/signals && pnpm exec vitest run --maxWorkers=2).
  • Solid wrapper: 833 passed, across 42 files (cd packages/solid && pnpm exec vitest run test --maxWorkers=1).
  • Signals development/production/observe builds and declaration generation, formatting, generated rules index, diff and release-invariant checks pass.
  • Final production core/store/verdict scenarios: 20,983 / 45,298 / 28,182 minified bytes, 7,647 / 14,902 / 9,961 Brotli bytes. All three exceed the unchanged size caps. These are total candidate sizes, not savings.

The new regressions cover held rejection/recovery, same-value recovery, latest derivations, projections, snapshot outcomes, first-flight/seed behavior, promise delivery, comparator cleanup, writable outcomes, arbitrary thrown causes and fulfilled Error data. Upstream failure reproductions used unmodified next source; the loading-seed rule is identified above as a contract change rather than an existing upstream guarantee.

Earlier connected-Chromium checks cover compiled JSX and held DOM/outside-read correspondence. They were not rerun after this final rebase, and the older harness uses a pinned runtime with selected outcome-file overlays; it is not full current-next browser validation. Native-compiler web suites were not run because the local native binding is unavailable. No formal proof, memory saving or throughput improvement is claimed.

Keep published failures separate from proposed computation status so held
rejection, retry and recovery obey the selected frame's publication rules.
Preserve successful memo history and loading seeds independently of accessor
outcomes, including first failures and captured snapshots.

Apply terminal delivery gates to both promise outcomes, share effect execution
and projection failure notification, and preserve original thrown causes when
diagnostic formatting fails. Follow next's first-load direction rule for both
successful and failed answers created over a live hold.

Add regressions for outside reads, latest derivations, same-value recovery,
first-outcome timing, snapshots, projections, promise delivery and error data.
@changeset-bot

changeset-bot Bot commented Oct 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7b5d641

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
@solidjs/signals Patch
test-integration Patch
@solidjs/web Patch
@solidjs/babel-plugin Patch
@solidjs/compiler Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
solid-js Patch
@solidjs/universal Patch
todos-server-example Patch

Not sure what this means? Click here to learn what changesets are.

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

@codspeed

codspeed Bot commented Oct 8, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 18.34%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 3 regressed benchmarks
✅ 184 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ build + consume 280.3 µs 604.2 µs -53.6%
❌ construct 32 µs 33.7 µs -5.1%
❌ construct 31.8 µs 33.5 µs -5.06%
⚡ ownKeys 266.9 µs 250.9 µs +6.39%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing GabbeV:fix/latest-settled-error (7b5d641) with next (e799cbd)

Open in CodSpeed

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