Skip to content

fix(signals): an error staged on its source keeps <Errored> waiting (#4364) - #4387

Merged
ryansolid merged 3 commits into
nextfrom
claude/project-thread-x1ore2-4364
Oct 28, 2026
Merged

ryansolid merged 3 commits into
nextfrom
claude/project-thread-x1ore2-4364

Conversation

@ryansolid

@ryansolid ryansolid commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Summary

Fixes #4364

Before: an uninitialized async memo re-runs synchronously after a write, and the re-run throws from a dependency. The error is lost, and <Loading> reveals an empty hole (<span></span>) instead of <Errored> showing its fallback.

Root cause: the dependency's throw happens in a flush that carries a write, so its error is staged on the source (D-41, stageError). The source gets CONFIG_STAGED_ERROR and _pendingValue = StatusError, and it only gets STATUS_ERROR at the commit. One of the source's readers throws that same error at once, either because:

  • it is a render effect reading a memo born staged, or
  • it is the memo itself reading the source through latest() (a verdict-lane pass).

That error reaches <Errored> naming the source. The boundary's output pass (prune) then sees the source without STATUS_ERROR, takes it as settled, drops it from _readers, and shows the content. When the commit later publishes the error, every reader already holds it, so nothing notifies the boundary again. The content stays revealed with a hole.

Fix: in boundaries.ts prune, an error boundary now treats a reader whose error is staged (CONFIG_STAGED_ERROR) as unsettled, the same as STATUS_ERROR. The source stays in the boundary's reader set until its error commits.

This also inlines the one-line unsettled helper (types are only STATUS_PENDING / STATUS_ERROR, so b._type | STATUS_PENDING is the same mask) and folds the loop's two delete calls into one. The change is about −12 B minified (see below). No new exports, no new nodes or effects. Loading boundaries are untouched: the new check is gated on mask & STATUS_ERROR.

Of the suspects in the issue:

  • stageError / handleError on the pending node: they do the right thing; the error is staged by design (D-41).
  • Dependency-tail trimming: not involved.
  • latest() running ahead: only one of the two paths into the bug. The m0() shape with a render effect reading a node born staged hits it too.

How did you test this change?

New tests, each failing on next and passing with the fix:

  • packages/signals/tests/errored-staged-source-4364.test.ts: the issue's graph under <Errored><Loading>…, over three shapes (m0() + s(), latest(m0), m0()). It expects [L], then [E boom] after the write, and [E boom] still after the async gate releases. On next, the first two shapes fail (the content reveals).
  • packages/web/test/errored-staged-source-4364.spec.tsx: the JSX form of the repro. On next it renders <span></span>; with the fix it renders the Errored fallback.

Suites, run after merging origin/next (#4369, #4331, #4371) and a clean turbo build --force:

  • signals: 5827 passed. treeshake.test.ts failed once under full-suite load and passed 15/15 when rerun alone.
  • solid: 906/906 passed.
  • web default: 1511 passed.
  • web server: 1886 passed, 1 failed. That failure is the known server-functions-adapter-request one, which also fails on next.
  • web hydrate: 673 passed, 1 failed: served-error-no-reask › "[streamed] the content waits for its sheet". That test is flaky on next too, and the failing scenario has only a <Loading> boundary, which the new check never touches. With next's own boundaries.ts rebuilt in this worktree, it failed 4 of 5 runs: it passes alone, and fails when it runs after its [loaded] sibling.
  • turbo typecheck test-types: 23/23 passed.

Size (node size.mjs --no-gate --json + node gate.mjs): the gate passes. Minified deltas vs next; brotli moves by ±60 B of compression noise. Hello world and the frozen core floors are unchanged.

scenario minified
signals: core floor ±0
signals: + createStore −19
app: hydrating (no stores) Show/For/Loading/Errored/lazy −12
app: hydrating + every store family −31
app: CSR Show/For/Loading/Errored/lazy −12
app: CSR observe tier −12
app: CSR observe + attribution −12
app: compiled CSR todo −31
app: compiled hydrating todo −31
page: base server components −12
page: live server components −12
page: compiled base / compiled live −12 / −9
page: base + router / live + router −12 / −12

Generated by Claude Code

…4364)

An uninitialized async memo whose re-run throws from a dependency lost the
error, and Loading revealed an empty hole. The dependency's error was
staged for the commit (D-41), but a reader threw it at once: a render
effect reading a memo born staged, or the memo itself through latest().
That error reached <Errored> naming the source. The boundary's output pass
then took the source as settled, because its error was only staged, so it
dropped the source and showed the content. When the commit published the
error, every reader already held it, and nothing reached the boundary
again.

prune() now treats a staged error on an error boundary's source as
unsettled. It also inlines the one-line `unsettled` helper and restructures
the loop, so the change is -12 B minified.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
@changeset-bot

changeset-bot Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 623ec33

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

@github-actions

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.41 KB 0 B 0 B −166 B 7.44 KB ✅
signals: + createStore 14.92 KB 0 B 0 B −1414 B 15.04 KB ✅
signals: + isPending/latest 10.38 KB 0 B 0 B 0 B 10.38 KB ✅
signals: + until/refresh (truth readers, no optimism) 8.04 KB 0 B 0 B −170 B 8.05 KB ✅
app: render + one signal (the simple-app floor) 9.77 KB 0 B 0 B −226 B 9.75 KB ⚠️ over by 19 B, 246 B minified headroom
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 18.67 KB +31 B (+0.2%) −12 B −258 B 18.49 KB ⚠️ over by 180 B, 278 B minified headroom lazy-page.js 0.04 KB
app: hydrating + every store primitive family 31.77 KB −12 B (−0.0%) −12 B −12 B 31.80 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.36 KB −57 B (−0.4%) −12 B −270 B 13.40 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 15.15 KB +21 B (+0.1%) −12 B −278 B 15.14 KB ⚠️ over by 14 B, 298 B minified headroom lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 29.67 KB −10 B (−0.0%) −12 B −277 B 29.70 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 9.93 KB 0 B 0 B −225 B 9.93 KB ⚠️ over by 3 B, 245 B minified headroom
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 26.19 KB +10 B (+0.0%) −12 B −606 B 26.27 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 32.39 KB +50 B (+0.2%) −12 B −536 B 32.25 KB ⚠️ over by 138 B, 556 B minified headroom stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.75 KB 0 B 0 B 0 B 11.76 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 33.52 KB −54 B (−0.2%) −12 B −196 B 33.37 KB ⚠️ over by 145 B, 216 B minified headroom assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, trace.js 9.01 KB, wire.js 3.47 KB
page: live server components (base + live/GET + action + isPending/latest) 38.02 KB +36 B (+0.1%) −12 B −92 B 37.75 KB ⚠️ over by 269 B, 112 B minified headroom assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, trace.js 9.00 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.59 KB +37 B (+0.1%) −12 B −185 B 35.43 KB ⚠️ over by 159 B, 205 B minified headroom assets.js 0.78 KB, decode.js 7.09 KB, regions.js 1.06 KB, sc-comments.js 0.20 KB, trace.js 8.94 KB, wire.js 3.47 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 41.85 KB +15 B (+0.0%) −9 B −9 B 41.56 KB ⚠️ over by 286 B, 29 B minified headroom eager (counted): web.js 22.35 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.05 KB, wire.js 3.47 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 41.04 KB +38 B (+0.1%) −12 B −196 B 40.81 KB ⚠️ over by 226 B, 216 B minified headroom assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, server.js 1.03 KB, serverForms.js 3.59 KB, trace.js 8.98 KB, wire.js 3.48 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 47.51 KB +42 B (+0.1%) −12 B −12 B 47.16 KB ⚠️ over by 347 B, 32 B minified headroom eager (counted): client.js 27.52 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 8.99 KB, wire.js 3.49 KB
server: floor (getRequestEvent + isServer) 1.27 KB 0 B 0 B −69 B 1.34 KB ✅
server: renderToString (the server-render floor) 22.79 KB 0 B 0 B 0 B 22.80 KB ✅

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

  • app: render + one signal (the simple-app floor): over brotli cap by 19 B; minified 26,803 B vs 27,029 B recorded with the cap (−226 B) — 246 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: hydrating (no stores) with Show/For/Loading/Errored/lazy: over brotli cap by 180 B; minified 53,362 B vs 53,620 B recorded with the cap (−258 B) — 278 B of the 20 B minified allowance left; −12 B minified over this PR's base
  • app: CSR, observe tier (same app on the observe artifacts): over brotli cap by 14 B; minified 41,885 B vs 42,163 B recorded with the cap (−278 B) — 298 B of the 20 B minified allowance left; −12 B minified over this PR's base
  • app: compiled floor (one template, one text hole, one delegated click): over brotli cap by 3 B; minified 27,335 B vs 27,560 B recorded with the cap (−225 B) — 245 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable): over brotli cap by 138 B; minified 100,508 B vs 101,044 B recorded with the cap (−536 B) — 556 B of the 20 B minified allowance left; −12 B minified over this PR's base
  • page: base server components (hydrating + dynamic + frames + sf reference): over brotli cap by 145 B; minified 101,363 B vs 101,559 B recorded with the cap (−196 B) — 216 B of the 20 B minified allowance left; −12 B minified over this PR's base
  • page: live server components (base + live/GET + action + isPending/latest): over brotli cap by 269 B; minified 115,125 B vs 115,217 B recorded with the cap (−92 B) — 112 B of the 20 B minified allowance left; −12 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 159 B; minified 107,733 B vs 107,918 B recorded with the cap (−185 B) — 205 B of the 20 B minified allowance left; −12 B minified over this PR's base
  • page: compiled live server components (the compiled base page + live/GET + action + isPending/latest): over brotli cap by 286 B; minified 123,173 B vs 123,182 B recorded with the cap (−9 B) — 29 B of the 20 B minified allowance left; −9 B minified over this PR's base
  • page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate): over brotli cap by 226 B; minified 125,191 B vs 125,387 B recorded with the cap (−196 B) — 216 B of the 20 B minified allowance left; −12 B minified over this PR's base
  • page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate): over brotli cap by 347 B; minified 140,430 B vs 140,442 B recorded with the cap (−12 B) — 32 B of the 20 B minified allowance left; −12 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 28, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 40766104977

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.522%

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: 1354
Covered Lines: 1124
Line Coverage: 83.01%
Relevant Branches: 1081
Covered Branches: 788
Branch Coverage: 72.9%
Branches in Coverage %: Yes
Coverage Strength: 29.92 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

1 warning

⚠️ 4 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

✅ 200 untouched benchmarks
⏩ 5 skipped benchmarks1


Comparing claude/project-thread-x1ore2-4364 (623ec33) with next (40cde2c)

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. ↩

@ryansolid
ryansolid marked this pull request as ready for review October 28, 2026 20:31
@ryansolid
ryansolid merged commit 2861ab8 into next Oct 28, 2026
9 checks passed
ryansolid pushed a commit that referenced this pull request Oct 28, 2026
Fixed on next by #4364's prune change (#4387): a staged error on an error
boundary's source keeps the boundary waiting. These pins fail on next @
40cde2c (before #4387) and pass from 2861ab8 on.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
ryansolid added a commit that referenced this pull request Oct 28, 2026
…es) (#4386)

Fixed on next by #4364's prune change (#4387): a staged error on an error
boundary's source keeps the boundary waiting. These pins fail on next @
40cde2c (before #4387) and pass from 2861ab8 on.


Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2

Co-authored-by: Claude <[email protected]>
@ryansolid
ryansolid deleted the claude/project-thread-x1ore2-4364 branch November 5, 2026 17:34
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