Skip to content

fix(signals): an Errored recovery re-arms the Loadings under it (#4627) - #4634

Merged
ryansolid merged 7 commits into
nextfrom
claude/fix-4627-errored-nested-loading
Nov 2, 2026
Merged

ryansolid merged 7 commits into
nextfrom
claude/fix-4627-errored-nested-loading

Conversation

@ryansolid

@ryansolid ryansolid commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Summary

Fixes #4627.

This follows a maintainer ruling from 2026-11-02 that changes the #1831 behaviour from #4401.

Why the change. While an <Errored> shows its fallback, nothing under it is on screen. Its recovery therefore re-arms the whole subtree: every <Loading> under it counts as not having shown content, even one that showed content before the error. Once the errored source has its new answer, each Loading catches what is still pending in the replacement frame. It shows its own fallback beside the recovered content ([Crash][finding related]). Before this change, the error stayed up until the nested content was ready.

Where the error still stays up (D-39, D-41):

  • the errored source's own re-ask;
  • a pending read directly under the <Errored> with no <Loading> between (a pending is not a value);
  • a recovery that is part of a held write or action, which reveals with that hold.

Mechanism (boundaries.ts):

  • isCollecting is a pure query. A <Loading> behind a shown error fallback (behindError) counts as collecting.
  • The reset to not-shown happens only where a Loading actually catches (caught()), so the recovery's pending reads are caught by the inner Loading, not forwarded into the write's hold.
  • In prune, caughtBelow lets an error boundary drop a reader that such a Loading catches. It applies only once the errored source has answered, is neither errored nor re-asking, and the Loading's swap is not staged in a hold.
    • That last condition is async-gen seed 3289 sass/scss support? #122. A recovery held by an action, through a latest() memo, used to attach the Loading's committed content while its swap was parked in the hold, briefly showing a bare <span></span>. The error now stays up until the action lands (D-39, D-41).
    • The regression tests are a new row in errored-recovery-fresh-loading-4627.test.ts and web/test/async-gen/e1-3289-122.spec.ts.
  • The loop in caught() that recorded pending readers on error parents is gone.

F-5 gets one exception pointing to D-39, in maintainer-approved wording: behind a shown error fallback nothing is on screen, so the recovery re-arms a <Loading> there.

Rules check. Every changed pin was checked against F-1, F-4, F-5, F-12, D-39 and D-41. F-5 and D-39 are amended with the ruling. S-38 is updated in SCENARIOS.md. The rules index is regenerated.

Pins changed:

Pin Before Now Rule
latest-memo-inline-parity.test.ts and web twin, #1831 rows (grouped and split) error held until m1's re-ask lands the inner Loading's fallback, then the content; grouped and split equal D-39 (re-ruled), F-4
derived-store-recovery-wake.test.ts "write" and "action" (seed 3289, #1831's store twin) error, v:5, content error, loading, v:5, content. In "action" the error stays through the hold (D-41), and the fallback shows at the commit because an armed Loading's pending holds nothing outside it (F-4) D-39, D-41, F-4
errored-recovery-stale-memo-4592 "inside Loading", signals and web error until a:1 lands loading while a:1 is out. The no-boundary rows are unchanged: the error stays D-39, F-1
store/derived-store-heal-4451.test.ts, async-gen/e3-derived-store-heal-4451.spec.ts updated to the re-arm screens D-39
web/test/errored-recovery-nested-loading-4425.spec.tsx test.fails (known bug) passes, unpinned D-39

How did you test this change?


Generated by Claude Code

While an <Errored> shows its fallback nothing under it is on screen, so
every <Loading> under it counts as not shown. On recovery each catches what
is still pending in the replacement frame and shows its fallback beside the
recovered content. The error still stays up for the errored source's own
re-ask, a pending read directly under the Errored, and a held recovery.

Re-rules async-gen 91501 #1831 (D-39, ruled 2026-11-02) and fixes the
#4425 known-bug spec.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117BoWNSSgtemsSYsaX2hXE
…#4627)

The derived-store recovery rows (seed 3289, the #1831 store twin) and the
#4592 inside-Loading rows follow the 2026-11-02 ruling: the errored source
answers, and what is still pending is caught by the re-armed Loading (D-39,
F-4). Held in an action, the error stays through the hold (D-41). The
no-boundary #4592 rows keep the error (a pending is not a value).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117BoWNSSgtemsSYsaX2hXE
@changeset-bot

changeset-bot Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 982be38

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

This PR includes changesets to release 15 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
board-example Patch
canvas-example Patch
server-dbmon-example 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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117BoWNSSgtemsSYsaX2hXE
@github-actions

github-actions Bot commented Nov 2, 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.42 KB 0 B 0 B −91 B 7.43 KB ✅
signals: + createStore 15.01 KB +1 B (+0.0%) +69 B +69 B 15.03 KB ✅
signals: + isPending/latest 11.86 KB 0 B 0 B 0 B 11.87 KB ✅
signals: + until/refresh (truth readers, no optimism) 8.05 KB 0 B 0 B −84 B 8.06 KB ✅
app: render + one signal (the simple-app floor) 9.80 KB 0 B 0 B −150 B 9.80 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 18.93 KB +48 B (+0.3%) +161 B +161 B 18.93 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 32.95 KB +116 B (+0.4%) +224 B +134 B 32.90 KB ❌ over by 50 B lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.63 KB +70 B (+0.5%) +160 B +160 B 13.57 KB ❌ over by 57 B lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 15.38 KB +79 B (+0.5%) +160 B +160 B 15.33 KB ❌ over by 54 B lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 30.02 KB +92 B (+0.3%) +160 B +160 B 29.94 KB ❌ over by 75 B lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 9.97 KB 0 B 0 B −187 B 9.98 KB ✅
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 26.49 KB +97 B (+0.4%) +228 B +133 B 26.41 KB ❌ over by 83 B stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 32.77 KB +132 B (+0.4%) +231 B +135 B 32.66 KB ❌ over by 114.00000000000364 B stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.75 KB 0 B 0 B 0 B 11.77 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 33.90 KB +35 B (+0.1%) +150 B +150 B 33.91 KB ✅ assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, trace.js 9.22 KB, wire.js 3.47 KB
page: live server components (base + live/GET + action + isPending/latest) 39.81 KB +50 B (+0.1%) +160 B +160 B 39.77 KB ❌ over by 39 B assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, trace.js 9.24 KB, wire.js 3.47 KB
page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread) 35.86 KB −50 B (−0.1%) +160 B +160 B 35.93 KB ✅ assets.js 0.78 KB, decode.js 7.09 KB, regions.js 1.06 KB, sc-comments.js 0.20 KB, trace.js 9.17 KB, wire.js 3.47 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 43.82 KB 0 B +160 B +160 B 43.84 KB ✅ eager (counted): web.js 23.34 KB; assets.js 0.78 KB, decode.js 7.09 KB, regions.js 1.07 KB, sc-comments.js 0.19 KB, trace.js 9.22 KB, wire.js 3.47 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 41.52 KB +107 B (+0.3%) +160 B +160 B 41.43 KB ❌ over by 90 B assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.07 KB, server.js 1.03 KB, serverForms.js 3.58 KB, trace.js 9.20 KB, wire.js 3.48 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 49.56 KB +51 B (+0.1%) +160 B +160 B 49.53 KB ❌ over by 34 B eager (counted): client.js 28.57 KB; assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.07 KB, server.js 1.03 KB, serverForms.js 3.30 KB, trace.js 9.22 KB, wire.js 3.49 KB
server: floor (getRequestEvent + isServer) 1.27 KB 0 B 0 B 0 B 1.29 KB ✅
server: renderToString (the server-render floor) 22.86 KB 0 B 0 B 0 B 22.87 KB ✅

❌ Fails the gate

  • app: hydrating + every store primitive family: over brotli cap by 50 B; minified 99,297 B vs 99,163 B recorded with the cap (+134 B) — 114 B past the 20 B minified allowance; +224 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • app: CSR with Show/For/Loading/Errored/lazy: over brotli cap by 57 B; minified 37,303 B vs 37,143 B recorded with the cap (+160 B) — 140 B past the 20 B minified allowance; +160 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • app: CSR, observe tier (same app on the observe artifacts): over brotli cap by 54 B; minified 42,253 B vs 42,093 B recorded with the cap (+160 B) — 140 B past the 20 B minified allowance; +160 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • app: CSR, observe tier + attribution engine enabled: over brotli cap by 75 B; minified 88,582 B vs 88,422 B recorded with the cap (+160 B) — 140 B past the 20 B minified allowance; +160 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store): over brotli cap by 83 B; minified 80,240 B vs 80,107 B recorded with the cap (+133 B) — 113 B past the 20 B minified allowance; +228 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable): over brotli cap by 114.00000000000364 B; minified 100,960 B vs 100,825 B recorded with the cap (+135 B) — 115 B past the 20 B minified allowance; +231 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • page: live server components (base + live/GET + action + isPending/latest): over brotli cap by 39 B; minified 119,903 B vs 119,743 B recorded with the cap (+160 B) — 140 B past the 20 B minified allowance; +160 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate): over brotli cap by 90 B; minified 126,040 B vs 125,880 B recorded with the cap (+160 B) — 140 B past the 20 B minified allowance; +160 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate): over brotli cap by 34 B; minified 145,716 B vs 145,556 B recorded with the cap (+160 B) — 140 B past the 20 B minified allowance; +160 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)

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 Nov 2, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 41545155667

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage remained the same at 78.205%

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: 1369
Covered Lines: 1131
Line Coverage: 82.62%
Relevant Branches: 1104
Covered Branches: 803
Branch Coverage: 72.74%
Branches in Coverage %: Yes
Coverage Strength: 30.1 hits per line

💛 - Coveralls

…e asked

Drops the child-boundary tracking (`_kids`, `unshow`, `reshow`): a
loading boundary behind a shown error fallback is collecting, and is reset
to not-shown when asked (`isCollecting`). Same screens; about half the
bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117BoWNSSgtemsSYsaX2hXE

Copy link
Copy Markdown
Member Author

The Size check job is red because of this PR's own growth: +208 to +227 B minified and at most +82 B brotli on the app and page rows, 0 B on hello world. The PR is waiting on the maintainer's call on that growth. Per the batch process, fix PRs don't edit scripts/size, and the caps get re-recorded once after the merge batch.


Generated by Claude Code

@codspeed

codspeed Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 12.3%

2 warnings

⚠️ 2 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

⚠️ 1 benchmark with a significant change ran on different hardware

Its base and head results were measured on machines with a different CPU, core count or memory, so the change might not come from the code. Run base and head on the same runner type.

Learn more about Macro Runners

See affected benchmarks

❌ 1 regressed benchmark
✅ 199 untouched benchmarks
⏩ 5 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ projection derive: write one NESTED field (reference) 2.1 ms 2.4 ms -12.3%

Tip

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


Comparing claude/fix-4627-errored-nested-loading (982be38) with next (3ca5262)2

Open in CodSpeed

Footnotes

  1. 5 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

  2. No successful run was found on next (efd7f7d) during the generation of this report, so 3ca5262 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

…ld (async-gen 3289 #122)

A recovery written in the tick an action opened is the action's (O1) and
reveals with its hold (D-39, D-41). When the errored memo reads `latest()`,
its recovery runs in the verdict lane, so the Errored took its source as
settled. The re-armed Loading's swap to its fallback was staged in the
action's hold, but the Errored's own swap ran mainline the next round. The
leaf then attached from the Loading's committed content while its effect's
run was parked, which left a bare `<span></span>` (F-1). `caughtBelow` now
holds the error while the catching Loading's output is held.

Review follow-ups: `isCollecting` is a pure query again. The re-arm reset
(`_initialized = false`) now happens in `caught`, where a Loading actually
catches. `behindError` is now a recursive arrow (min 5844 -> 5819).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117BoWNSSgtemsSYsaX2hXE
Approved wording (maintainer, 2026-11-02).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117BoWNSSgtemsSYsaX2hXE

Copy link
Copy Markdown
Member Author

The CodSpeed flag on d631d3c (projection derive: write one NESTED field (reference), −12.33%) is noise. CodSpeed reports that base and head ran on different hardware. The bench never creates a boundary, so it never reaches this PR's code in boundaries.ts. It's also a known module-size-sensitive bench that swings ±13 to 15% (lazy GC sweep timing), and it flagged #4633 the same way.


Generated by Claude Code

@ryansolid
ryansolid merged commit 4235019 into next Nov 2, 2026
7 of 9 checks passed
ryansolid added a commit that referenced this pull request Nov 3, 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.

3 participants