Repository navigation
fix(signals): a held error waits for its frame (D-41) - #4085
Conversation
Prototype for the proposed D-40: an error a held pass finds is staged in _pendingValue (CONFIG_STAGED_ERROR), propagated to dependents as a staging of their own, and published by commitPendingNode when the frame lands. A held recovery keeps the shown error until its landing; a re-ask of the source whose error a node shows re-runs it on the settle. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q7JD9PktsxtBJJbWmtuwr
… and pins An error settles the async its node asked for, so a hold never waits on it; but settled is not shown. A value landing on a held node is staged until the frame reveals, an error publishes at once. F-1 and F-2 already decide this; D-40 states it as a narrowing of F-12, S-41/S-42 are the screens, Q-6 puts the two columns to the maintainer, Q-7 the loadingValue contract question #3886 raised beside it. Pinned it.fails. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q7JD9PktsxtBJJbWmtuwr
…tus error Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q7JD9PktsxtBJJbWmtuwr
An error a held node's flight or pass finds is staged like a value and published by the frame's commit through the ordinary error path; the separate staged-error publish path is gone. A held recovery keeps the shown error until its own commit, a recovery equal to the last good value reaches effects, latest() throws a staged error (born held too), and watch() (resolve/until/refresh) rejects at the landing. Rules: D-40 (#3995, proposed) renumbered D-41 and ruled (Q-6 B); S-41/S-42 are S-42/S-43; Q-6 removed from OPEN-QUESTIONS. The derived-store untracked read is the open deviation, pinned it.fails. watch-error-outcomes.test.ts is from #3886. Co-authored-by: Gabriel Valfridsson <gabriel.valfridsson@gmail.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBzTHZM41ipGacdiFva1ju
🦋 Changeset detectedLatest commit: 116b4ef The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 packages
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 |
Size (brotli, eager entry chunk)
|
Truthiness checks for _shownError (always a status error when set), flushTransaction and passLane. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBzTHZM41ipGacdiFva1ju
|
The red Generated by Claude Code |
Merging this PR will regress 1 benchmark
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | reconcile: deep tree, 10 of ~12k paths subscribed |
3.1 ms | 3.2 ms | -5.28% |
| ⚡ | memo + sync render effect only (reference) |
31.2 ms | 27 ms | +15.36% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing fix/held-error-frame (116b4ef) with next (4a1ed17)
|
CodSpeed's one regression is Generated by Claude Code |
…hows it commitPendingNode publishes a staged error at the seam, before the clock advances. Readers re-derived by it (a boundary over the failed memo) run on the next cycle, where read() took the error for an old one and retried the async source; the retry rejected again and staged again, forever (web's errored-show-retry-3991 outside variant ran out of memory). Stamp the commit with the cycle that shows it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBzTHZM41ipGacdiFva1ju
Size-Exception: the held-error path costs +437 B minified / +167 B brotli on the core floor after a trim pass; each raised cap is measured + 10 B with a dated ledger line. The two compiled server-component pages stay (within their recorded minified). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBzTHZM41ipGacdiFva1ju
Conflicts: D-41/S-42/S-43 (errors) and next's D-40/S-41 (createDeferred, #4067) both sit after D-39/S-40; kept both, D-41 in section E and the new section F after it. Size caps re-raised from next's caps against next @ 4a1ed17 (same Size-Exception). CONFIG_STAGED_ERROR moves to 1 << 21: createDeferred's CONFIG_LAGS took 1 << 19, and a lagging node read as a staged error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBzTHZM41ipGacdiFva1ju
Coverage Report for CI Build 38700193946Coverage remained the same at 77.548%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Requested by Ryan · project thread
Summary
Before: when a transaction held a node's frame and that node errored, the error was published at once. A value in the same spot would have waited for the frame. As a result, the effect's error arm ran early,
<Errored>flipped beside the previous frame's content (F-1's torn screen), an untracked read threw,isPendingreadfalse, andresolve/until/refreshrejected early. A held recovery dropped the shown error ahead of its frame. A recovery equal to the last good value never reached the effect, held or not, because the equality gate swallowed it.After: an error is an answer (D-41, ruled 2026-10-15 as Q-6 B). It ends the wait like a value, and it shows when a value would. A held rejection or throw is staged and published by the frame's commit. Until then:
<Errored>keep the previous frameisPendingistruelatestthrows the proposed error, including for a first load born in the holdresolve/until/refreshreject at the landingA held recovery keeps the shown error until its own commit. A recovery to the last good value reaches effects.
How: the error is staged in
_pendingValue(CONFIG_STAGED_ERROR), andcommitPendingNodepublishes it through the ordinarynotifyStatuspath. This replaces the prototype's separate publish path (publishError,stageErrorOn, thestagedflag throughpropagateStatus)._shownErroron the node extension keeps the displayed error through a retry.watch()defers its rejection to the landing the way it defers a value.Rules: #3995's proposed D-40 is renumbered D-41 and marked ruled. Its scenarios S-41/S-42 become S-42/S-43, and Q-6 is removed from
OPEN-QUESTIONS.md. The new numbers avoid the D-40/S-41 that #4067 uses. Q-7 (loadingValueafter a shown failure) stays open, with today's behaviour unchanged.Open deviation, pinned
it.fails: an untracked read of a derived store's property (createStore(fn), projections) still throws a held rejection before its frame. The store's effect and the screen do wait.Not in this PR: the standalone bugs from #3886's audit, which have no rule of their own:
tests/watch-error-outcomes.test.tsis @GabbeV's, from #3886.Size: the core floor is +437 B minified / +167 B brotli against
next, after a trim pass. Every client scenario grows about the same. Ryan accepted the cost on 2026-10-15 (golf first, then raise by what is left), so the caps that fail the gate are raised to measured + 10 B, each with a dated ledger line. That includes the five frozen floors. The two compiled server-component pages stay as they are: they are over brotli but within their recorded minified sizes, so the gate only warns.Size-Exception: a held error waits for its frame (D-41), core error-path behaviour ruled for 2.0; maintainer accepted the floor cost on 2026-10-15 after a trim pass.
How did you test this change?
pnpm exec vitest runin packages/signals: 5257 passed, 3 expected fail.server-functions-adapter-request), which fails the same way onnextlocally. The first CI run here crashed the web job: a held error committed at the seam looked stale to the next cycle's readers, so a boundary re-deriving over it retried the failed memo forever. Fixed by stamping the commit with the cycle that shows it;errored-show-retry-3991.spec.tsxcovers it. The S-42/S-43 pins are plainit, and the derived-store pin isit.fails.scripts/size/size.mjsagainstnextat 3fc5551.🤖 Generated with Claude Code
https://claude.ai/code/session_01EBzTHZM41ipGacdiFva1ju
Generated by Claude Code
Generated by Claude Code