Skip to content

fix(signals): a value that replaces a held error is not thrown at the reveal - #4148

Closed
drakeo338 wants to merge 2 commits into
solidjs:nextfrom
drakeo338:fix/held-error-recovery
Closed

drakeo338 wants to merge 2 commits into
solidjs:nextfrom
drakeo338:fix/held-error-recovery

Conversation

@drakeo338

Copy link
Copy Markdown

Fixes #4145

Summary

When a memo throws while an async sibling holds the frame, stageError puts the error in _pendingValue and sets CONFIG_STAGED_ERROR. If the memo recovers before that frame commits, recompute stages the value over it but leaves the flag set. At the reveal, serve sees the flag and throws the value. That halts reactivity. The flag only got cleared in commitPendingNode, which runs after that read.

Now the branch that stages a pass's value also clears CONFIG_STAGED_ERROR.

How did you test this change?

New tests/held-error-recovery-4145.test.ts (the issue's repro, recovering to 1 and to 7). Before the fix, both cases halt with the value as the error and the sum stays 2. After it, they show 3 and 9.

pnpm build && pnpm test: signals 5355 passed. The only failure, in web's server-functions-adapter-request.spec.tsx, also fails on next without this change.

Written with help from an AI assistant (Claude). I reviewed and tested the change myself.

… reveal

A memo that throws while an async sibling holds the frame stages its
error (D-41): _pendingValue carries it and CONFIG_STAGED_ERROR marks it.
A successful pass in the same hold replaced the staging with its value
but left the flag set, so readers at the reveal threw the value as an
error and halted the reactive system. The pass that stages a value now
clears the flag.

Fixes solidjs#4145
@changeset-bot

changeset-bot Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bda2ad6

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

@codspeed

codspeed Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 10.57%

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

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ ⚠️ build + consume 694.8 µs 776.9 µs -10.57%

Tip

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


Comparing drakeo338:fix/held-error-recovery (bda2ad6) with next (fda29a4)

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

Copy link
Copy Markdown
Member

Thanks for this! Clearing CONFIG_STAGED_ERROR where recompute stages a pass's value is the right fix for #4145, and error → value → error inside one hold still throws the last error as it should. Two other paths stage a value over a held error and keep the stale flag, though:

  1. Async settle. setSignal(el, () => value) in async.ts stages the value without clearing the flag. Repro: a memo that throws at step 1 and returns inner.promise at step 2, beside the issue's held slow sibling. setStep(1), setStep(2), inner.resolve(5), request.resolve(2) ends in [REACTIVITY_HALTED] 5 (either resolve order).
  2. Writable memo set inside an action. action(function* () { setStep(1); yield; setValue(5); }) where step() === 1 throws: the effect's error arm receives 5.

Clearing the flag in setSignal right after el._pendingValue = v fixed both in our probes, with the signals suite still green. An option that might also be smaller: drop the flag and have the readers test _pendingValue instanceof StatusError, which covers every staging path at once.

Size: the Size check fails. It's +14 B minified everywhere, which puts signals: core floor and signals: + until/refresh at +22 B, 2 B past the 20 B allowance. Please trim under it, or add a Size-Exception: line to the PR body with the reason.

Could you add the two repros above as tests too? Note that #4143 touches the same staged-error area (setMemo), so whichever lands second may need a small follow-up.

— Claude via Claude Code


Generated by Claude Code

ryansolid pushed a commit that referenced this pull request Oct 19, 2026
CONFIG_STAGED_ERROR was cleared only at the commit. With a pass's own
throw now staged whenever a flush runs, a memo that throws and re-runs
in the same flush kept the flag: a value re-run was thrown as an error
(its raw value reached the error arm, #4145), and a pending re-run left
the superseded error to be published at the commit.

A pass now drops an error staged by an earlier pass before it runs:
its own answer (a value, a flight, or an error it stages again)
replaces it. This folds in #4148's clear on the value path.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
ryansolid added a commit that referenced this pull request Oct 19, 2026
…4109) (#4156)

* fix(signals): stage a pass's error before the flush joins its hold (#4109)

A synchronous memo whose pass threw in a flush published the error at once
unless the flush had already joined a hold. When the pass ran before its
async sibling went pending, the error showed ahead of the frame (D-41): an
untracked read threw while the screen still showed the committed value.
Stage it whenever the pass read a staging inside a flush; the seam holds
it with the frame, or the commit publishes it.

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

* perf(signals): gate the pass-staged error on a running flush alone (#4109)

flushTransaction implies nothing extra here once the pass is inside a
flush: the one check covers the #4109 case and keeps the core floor at
+2 B minified over next (was +7 B).

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

* fix(signals): a later pass replaces a staged error (#4109)

CONFIG_STAGED_ERROR was cleared only at the commit. With a pass's own
throw now staged whenever a flush runs, a memo that throws and re-runs
in the same flush kept the flag: a value re-run was thrown as an error
(its raw value reached the error arm, #4145), and a pending re-run left
the superseded error to be published at the commit.

A pass now drops an error staged by an earlier pass before it runs:
its own answer (a value, a flight, or an error it stages again)
replaces it. This folds in #4148's clear on the value path.

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

* chore(size): raise caps for the #4109 staged-error clear (Size-Exception)

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

---------

Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Member

Thank you for this, and for the clear repro. #4156, merged to next, now clears CONFIG_STAGED_ERROR at the start of every pass, which includes the clear this PR adds. #4145 no longer reproduces on next, and this PR's test passes there without the change, so I'm closing this one as superseded. The action-write case from the review is being fixed together with #4143.

— Claude via Claude Code


Generated by Claude Code

@ryansolid ryansolid closed this Oct 20, 2026
ryansolid added a commit that referenced this pull request Oct 20, 2026
…n read (#4110) (#4167)

* fix(signals): a write clears a writable memo's previous error

STATUS_ERROR was only cleared by a recompute or an async settle, neither of which a direct write performs, so an errored memo kept throwing the stale error to every later reader.

* fix(signals): keep a writable memo's error shown until the write commits (#4110)

Builds on #4143. The error stays the display until the write commits, so a
read between the write and its flush, or under the action holding it, still
throws instead of returning the old value. A write also clears an error a
pass staged under the same hold, so effects see the written value instead
of their error arm (the action-write case left from #4148).

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

* chore(size): raise caps for the #4110 writable-memo error fix

Accepted by the maintainer on 2026-10-20: about +50 B minified on the core
floor. Also re-records #4163's +9 B, which merged without a re-record.

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

---------

Co-authored-by: Nicholas <nickylin211@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
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