Skip to content

fix(signals): route async protocol throws and error-formatting failures to the memo (#4112) - #4158

Merged
ryansolid merged 5 commits into
nextfrom
claude/project-thread-x1ore2-4112
Oct 19, 2026
Merged

ryansolid merged 5 commits into
nextfrom
claude/project-thread-x1ore2-4112

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Summary

Fixes #4112.

Before: three kinds of failure escaped the memo's error handling.

  • A throw from a later next() (or a step's then) of an async iterator became an unhandled rejection, and the memo kept its last value.
  • A throw during deferred iterator setup (the [Symbol.asyncIterator] getter or factory, or the first next() after a thenable resolved to an iterable) left the memo stuck in NotReadyError.
  • StatusError formatting a null-prototype object, or an error with a throwing message getter, threw a TypeError out of createMemo that replaced the original error.

After: all three land on the memo and reach its error boundary. The original error is kept as cause (#2840).

How:

  • In consumeIterator's iterate() (src/core/async.ts), the protocol calls are wrapped so a throw takes the existing rejected-step path. That path rethrows during the initial read and otherwise calls handleError. The non-initial branch also gets the settleAutodispose() the async rejection handler already has.
  • The deferred fulfillment callback guards flattenIfIterable and calls handleError plus settleAutodispose on a throw.
  • StatusError (src/core/error.ts) formats the message best-effort, as proposed in fix(signals): publish value and error outcomes consistently #3886, and keeps cause: original.

When a protocol call throws, the iterator is treated as completed and return() is not called. That matches for await.

How did you test this change?

  • New packages/signals/tests/async-protocol-throw-4112.test.ts, 7 cases: one control (a rejected next() reaches the boundary), four async escape cases checked through a createErrorBoundary fallback with no unhandled rejections allowed, and two formatting cases. Without the fix, 6 fail and the control passes. With it, all 7 pass.
  • packages/signals full run: 5350 passed. 13 failed, all timeouts in timing- and dist-heavy files on an overloaded machine; those files pass when run serially (76/76).

Generated by Claude Code

… the memo (#4112)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
@changeset-bot

changeset-bot Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d17154e

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

This PR includes changesets to release 14 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
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

github-actions Bot commented Oct 19, 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.28 KB +26 B (+0.4%) +3 B +3 B 7.27 KB ⚠️ over by 13 B, 17 B minified headroom
signals: + createStore 14.83 KB +33 B (+0.2%) +3 B +3 B 14.82 KB ⚠️ over by 11 B, 17 B minified headroom
signals: + isPending/latest 10.05 KB +29 B (+0.3%) +3 B +3 B 10.03 KB ⚠️ over by 22 B, 17 B minified headroom
signals: + until/refresh (truth readers, no optimism) 7.89 KB +23 B (+0.3%) +3 B +3 B 7.88 KB ⚠️ over by 6 B, 17 B minified headroom
app: render + one signal (the simple-app floor) 9.61 KB +34 B (+0.4%) +3 B +3 B 9.59 KB ⚠️ over by 20 B, 17 B minified headroom
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 18.25 KB +26 B (+0.1%) +3 B +3 B 18.24 KB ⚠️ over by 12 B, 17 B minified headroom lazy-page.js 0.04 KB
app: hydrating + every store primitive family 30.22 KB +2 B (+0.0%) +3 B +3 B 30.23 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.22 KB +46 B (+0.3%) +3 B +3 B 13.19 KB ⚠️ over by 29 B, 17 B minified headroom lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 14.93 KB −13 B (−0.1%) +3 B +3 B 14.95 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 29.50 KB +6 B (+0.0%) +3 B +2 B 29.48 KB ⚠️ over by 20 B, 18 B minified headroom lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 9.81 KB +31 B (+0.3%) +3 B +3 B 9.79 KB ⚠️ over by 16 B, 17 B minified headroom
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 26.04 KB +47 B (+0.2%) +3 B +3 B 26.03 KB ⚠️ over by 6 B, 17 B minified headroom stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 32.05 KB +79 B (+0.2%) +3 B +3 B 31.98 KB ⚠️ over by 71 B, 17 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.11 KB −1 B (−0.0%) +3 B +3 B 33.12 KB ✅ assets.js 0.78 KB, decode.js 6.25 KB, lazy-page.js 0.04 KB, regions.js 1.07 KB, trace.js 9.03 KB, wire.js 2.78 KB
page: live server components (base + live/GET + action + isPending/latest) 37.36 KB +18 B (+0.0%) +3 B +3 B 37.37 KB ✅ assets.js 0.78 KB, decode.js 6.25 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, trace.js 9.00 KB, wire.js 2.78 KB
page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread) 35.18 KB +29 B (+0.1%) +3 B +3 B 35.16 KB ⚠️ over by 17 B, 17 B minified headroom assets.js 0.78 KB, decode.js 6.25 KB, regions.js 1.06 KB, sc-comments.js 0.20 KB, trace.js 9.04 KB, wire.js 2.77 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 41.24 KB +30 B (+0.1%) +3 B +3 B 41.23 KB ⚠️ over by 15 B, 17 B minified headroom eager (counted): web.js 21.86 KB; assets.js 0.78 KB, decode.js 6.25 KB, regions.js 1.06 KB, sc-comments.js 0.19 KB, trace.js 9.07 KB, wire.js 2.77 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 40.61 KB +75 B (+0.2%) +3 B +3 B 40.58 KB ⚠️ over by 33 B, 17 B minified headroom assets.js 0.78 KB, decode.js 6.25 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, server.js 1.02 KB, serverForms.js 3.59 KB, trace.js 9.11 KB, wire.js 2.77 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 46.87 KB +91 B (+0.2%) +3 B +3 B 46.79 KB ⚠️ over by 77 B, 17 B minified headroom eager (counted): client.js 27.06 KB; assets.js 0.78 KB, decode.js 6.25 KB, lazy-page.js 0.04 KB, regions.js 1.07 KB, server.js 1.02 KB, serverForms.js 3.30 KB, trace.js 9.05 KB, wire.js 2.80 KB
server: floor (getRequestEvent + isServer) 1.33 KB 0 B 0 B 0 B 1.34 KB ✅
server: renderToString (the server-render floor) 20.62 KB 0 B 0 B −23 B 20.59 KB ⚠️ over by 34 B, 43 B minified headroom

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

  • signals: core floor (createSignal/Memo/Effect/Root/flush): over brotli cap by 13 B; minified 19,830 B vs 19,827 B recorded with the cap (+3 B) — 17 B of the 20 B minified allowance left; +3 B minified over this PR's base
  • signals: + createStore: over brotli cap by 11 B; minified 44,927 B vs 44,924 B recorded with the cap (+3 B) — 17 B of the 20 B minified allowance left; +3 B minified over this PR's base
  • signals: + isPending/latest: over brotli cap by 22 B; minified 28,250 B vs 28,247 B recorded with the cap (+3 B) — 17 B of the 20 B minified allowance left; +3 B minified over this PR's base
  • signals: + until/refresh (truth readers, no optimism): over brotli cap by 6 B; minified 21,642 B vs 21,639 B recorded with the cap (+3 B) — 17 B of the 20 B minified allowance left; +3 B minified over this PR's base
  • app: render + one signal (the simple-app floor): over brotli cap by 20 B; minified 26,718 B vs 26,715 B recorded with the cap (+3 B) — 17 B of the 20 B minified allowance left; +3 B minified over this PR's base
  • app: hydrating (no stores) with Show/For/Loading/Errored/lazy: over brotli cap by 12 B; minified 53,294 B vs 53,291 B recorded with the cap (+3 B) — 17 B of the 20 B minified allowance left; +3 B minified over this PR's base
  • app: CSR with Show/For/Loading/Errored/lazy: over brotli cap by 29 B; minified 36,951 B vs 36,948 B recorded with the cap (+3 B) — 17 B of the 20 B minified allowance left; +3 B minified over this PR's base
  • app: CSR, observe tier + attribution engine enabled: over brotli cap by 20 B; minified 88,237 B vs 88,235 B recorded with the cap (+2 B) — 18 B of the 20 B minified allowance left; +3 B minified over this PR's base
  • app: compiled floor (one template, one text hole, one delegated click): over brotli cap by 16 B; minified 27,250 B vs 27,247 B recorded with the cap (+3 B) — 17 B of the 20 B minified allowance left; +3 B minified over this PR's base
  • app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store): over brotli cap by 6 B; minified 80,932 B vs 80,929 B recorded with the cap (+3 B) — 17 B of the 20 B minified allowance left; +3 B minified over this PR's base
  • app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable): over brotli cap by 71 B; minified 101,503 B vs 101,500 B recorded with the cap (+3 B) — 17 B of the 20 B minified allowance left; +3 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 17 B; minified 107,591 B vs 107,588 B recorded with the cap (+3 B) — 17 B of the 20 B minified allowance left; +3 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 15 B; minified 122,769 B vs 122,766 B recorded with the cap (+3 B) — 17 B of the 20 B minified allowance left; +3 B minified over this PR's base
  • page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate): over brotli cap by 33 B; minified 125,077 B vs 125,074 B recorded with the cap (+3 B) — 17 B of the 20 B minified allowance left; +3 B minified over this PR's base
  • page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate): over brotli cap by 77 B; minified 140,058 B vs 140,055 B recorded with the cap (+3 B) — 17 B of the 20 B minified allowance left; +3 B minified over this PR's base
  • server: renderToString (the server-render floor): over brotli cap by 34 B; minified 72,131 B vs 72,154 B recorded with the cap (−23 B) — 43 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 19, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 39331327442

Coverage remained the same at 77.555%

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: 1347
Covered Lines: 1103
Line Coverage: 81.89%
Relevant Branches: 1050
Covered Branches: 756
Branch Coverage: 72.0%
Branches in Coverage %: Yes
Coverage Strength: 27.68 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by 31.92%

⚠️ 2 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

⚠️ 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
✅ 195 untouched benchmarks
⏩ 5 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ projection derive: write one NESTED field (reference) 1,181 µs 895.3 µs +31.92%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/project-thread-x1ore2-4112 (d17154e) with next (8c75f82)

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

Guard only the async continuation sites (iterateOrRelease and the deferred
fulfillment callback) instead of the whole iterate() protocol block, and
drop registerDeferredClose in favour of cleanup(fn, owner).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
@ryansolid
ryansolid marked this pull request as ready for review October 19, 2026 22:32
@ryansolid
ryansolid merged commit a9b9f9e into next Oct 19, 2026
8 checks passed
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