Skip to content

fix(signals): a failed derived-store refetch reaches Errored (#3887) - #3917

Merged
ryansolid merged 2 commits into
nextfrom
fix/rc15-3887
Oct 8, 2026
Merged

ryansolid merged 2 commits into
nextfrom
fix/rc15-3887

Conversation

@ryansolid

@ryansolid ryansolid commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Fixes #3887

Thanks to @ethan-huo for the report.

Root cause

Before a derived store serves a value, pullFamily (packages/signals/src/store/store.ts) brings its derive, the family's firewall node, up to date. When the derive is pending or errored, the reader links to it and observes it like a memo reader would. The exception is a render effect outside the flight's own flush: it "keeps what it shows" and returns early, learning of the landing from the leaves the landing changes. That's right for a flight, which lands and changes leaves. But the same early return also fired when the derive had errored. A rejection has no landing and changes no leaves, so the render effect never read the error, and Errored never saw it. The UI kept showing the last value.

This hits any createStore(fn) whose re-fetch rejects: sync derive to rejecting async, and async to rejecting async. A plain memo reader has no such shortcut, so memos are fine.

Fix

Add !(fw._statusFlags & STATUS_ERROR) to the keep-the-frame condition, so an errored derive is read (readNode(fw) throws its error) and the error reaches the boundary. Pending derives behave exactly as before.

Test

packages/signals/tests/store/derived-refetch-error-3887.test.ts mounts the JSX shape at the signals level: a render effect reading the store under a loading boundary under an error boundary, with an outer render effect recording the boundary's output.

  • sync derive replaced by a rejecting async one: shows "error". Fails on next (stays "content").
  • async derive replaced by a rejecting async one: shows "error". Fails on next.
  • memo parity: the same with a memo. Passes before and after; it's the control.

I also ran the triage's JSX repros (<Errored><Loading><span>{store.value}</span>) against the fix in @solidjs/web, and all three pass. The full signals suite passes.

Related: draft #3886

@GabbeV's draft #3886 ("publish value and error outcomes consistently") also fixes this, as part of a much larger rework of how value and error outcomes publish. This PR is the minimal hotfix for rc.15 and doesn't touch #3886. If #3886 lands, its rework may subsume this condition. The test here is written against observable behaviour, so it should carry over unchanged to reconcile the two.

Size

From CI's Size job (Rolldown, brotli, eager entry chunk, vs base next @ 8d23a5a):

scenario vs base (brotli) minified vs base gate
signals: + createStore +8 B +10 B ✅
app: hydrating + every store primitive family −19 B +10 B ✅
app: compiled CSR (store) −1 B +10 B ✅
app: compiled hydrating (store) +58 B +10 B ⚠️ over brotli cap by 43 B, within the minified allowance (10 B of 20 B left)
every other scenario, including app: render + one signal (hello world) and signals: core floor 0 B 0 B ✅

The gate passes. The other ⚠️ page warnings are identical on next.

Public API changes

None.

pullFamily let a held render effect outside the flight's flush keep its
frame when the derive had errored too; a rejection has no landing to learn
of, so the error never reached the boundary.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@changeset-bot

changeset-bot Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2f412ef

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

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

Size (brotli, eager entry chunk)

scenario head vs base minified vs base minified vs recorded cap lazy chunks (not counted)
signals: core floor (createSignal/Memo/Effect/Root/flush) 7.43 KB 0 B 0 B +15 B 7.45 KB ✅
signals: + createStore 14.70 KB +8 B (+0.1%) +10 B +10 B 14.70 KB ✅
signals: + isPending/latest 9.63 KB 0 B 0 B +15 B 9.65 KB ✅
app: render + one signal (the simple-app floor) 9.92 KB 0 B 0 B +15 B 9.93 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 17.89 KB 0 B 0 B +15 B 17.91 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 29.15 KB −19 B (−0.1%) +10 B +65 B 29.19 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 12.92 KB 0 B 0 B +15 B 12.96 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 14.53 KB 0 B 0 B +15 B 14.53 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 28.85 KB 0 B 0 B +15 B 28.89 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 10.12 KB 0 B 0 B +15 B 10.13 KB ✅
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 25.22 KB −1 B (−0.0%) +10 B +65 B 25.24 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 31.21 KB +58 B (+0.2%) +10 B +10 B 31.17 KB ⚠️ over by 43 B, 10 B minified headroom stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.11 KB 0 B 0 B 0 B 11.13 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 33.91 KB 0 B 0 B +161 B 33.92 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.24 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, trace.js 8.21 KB, wire.js 0.93 KB
page: live server components (base + live/GET + action + isPending/latest) 37.61 KB 0 B 0 B +15 B 37.59 KB ⚠️ over by 20 B, 5 B minified headroom assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.24 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, trace.js 8.20 KB, wire.js 0.93 KB
page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread) 35.15 KB 0 B 0 B +15 B 35.13 KB ⚠️ over by 16 B, 5 B minified headroom assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.24 KB, regions.js 0.80 KB, sc-comments.js 0.20 KB, trace.js 8.20 KB, wire.js 0.93 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 40.65 KB 0 B 0 B +15 B 40.66 KB ✅ eager (counted): web.js 22.02 KB; assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.24 KB, regions.js 0.79 KB, sc-comments.js 0.19 KB, trace.js 8.21 KB, wire.js 0.93 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 46.00 KB 0 B 0 B +15 B 46.02 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.24 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, server.js 1.02 KB, trace.js 8.20 KB, wire.js 0.93 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 47.33 KB 0 B 0 B +15 B 47.29 KB ⚠️ over by 36 B, 5 B minified headroom assets.js 0.78 KB, bind.js 1.84 KB, decode.js 6.24 KB, lazy-page.js 0.04 KB, regions.js 0.81 KB, server.js 1.02 KB, trace.js 8.21 KB, wire.js 0.94 KB
server: floor (getRequestEvent + isServer) 1.33 KB 0 B 0 B 0 B 1.34 KB ✅
server: renderToString (the server-render floor) 20.40 KB 0 B 0 B +4 B 20.42 KB ✅

⚠️ Over the brotli cap within the minified allowance (passes)

  • app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable): over brotli cap by 43 B; minified 99,682 B vs 99,672 B recorded with the cap (+10 B) — 10 B of the 20 B minified allowance left; +10 B minified over this PR's base
  • page: live server components (base + live/GET + action + isPending/latest): over brotli cap by 20 B; minified 117,456 B vs 117,441 B recorded with the cap (+15 B) — 5 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread): over brotli cap by 16 B; minified 109,461 B vs 109,446 B recorded with the cap (+15 B) — 5 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate): over brotli cap by 36 B; minified 148,683 B vs 148,668 B recorded with the cap (+15 B) — 5 B of the 20 B minified allowance left; +0 B minified over this PR's base

Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. A scenario fails only when it is over its brotli cap and its minified size is more than 20 B over the minified recorded with the cap; over the cap within that allowance is brotli layout noise and passes with a warning. Caps and their recorded minified in scripts/size/scenarios.js; the floor and page caps in floor-caps.json are frozen (lower only, or Size-Exception: in the PR body). npm run ratchet lowers caps per RC; it never raises one (scripts/size/README.md).

@coveralls

coveralls commented Oct 8, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 37798443014

Coverage remained the same at 76.43%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1227
Covered Lines: 996
Line Coverage: 81.17%
Relevant Branches: 958
Covered Branches: 674
Branch Coverage: 70.35%
Branches in Coverage %: Yes
Coverage Strength: 28.3 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 188 untouched benchmarks


Comparing fix/rc15-3887 (2f412ef) with next (5a26ebe)

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.

2 participants