Repository navigation
fix(signals): an error staged on its source keeps <Errored> waiting (#4364) - #4387
Conversation
…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
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
🦋 Changeset detectedLatest commit: 623ec33 The changes in this PR will be included in the next version bump. This PR includes changesets to release 15 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)
|
Coverage Report for CI Build 40766104977Warning Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes. Coverage remained the same at 78.522%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will not alter performance1 warning
Comparing Footnotes
|
…uard (#4364) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
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
…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]>
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 getsCONFIG_STAGED_ERRORand_pendingValue = StatusError, and it only getsSTATUS_ERRORat the commit. One of the source's readers throws that same error at once, either because:latest()(a verdict-lane pass).That error reaches
<Errored>naming the source. The boundary's output pass (prune) then sees the source withoutSTATUS_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.tsprune, an error boundary now treats a reader whose error is staged (CONFIG_STAGED_ERROR) as unsettled, the same asSTATUS_ERROR. The source stays in the boundary's reader set until its error commits.This also inlines the one-line
unsettledhelper (types are onlySTATUS_PENDING/STATUS_ERROR, sob._type | STATUS_PENDINGis the same mask) and folds the loop's twodeletecalls 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 onmask & STATUS_ERROR.Of the suspects in the issue:
stageError/handleErroron the pending node: they do the right thing; the error is staged by design (D-41).latest()running ahead: only one of the two paths into the bug. Them0()shape with a render effect reading a node born staged hits it too.How did you test this change?
New tests, each failing on
nextand 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. Onnext, the first two shapes fail (the content reveals).packages/web/test/errored-staged-source-4364.spec.tsx: the JSX form of the repro. Onnextit renders<span></span>; with the fix it renders the Errored fallback.Suites, run after merging
origin/next(#4369, #4331, #4371) and a cleanturbo build --force:treeshake.test.tsfailed once under full-suite load and passed 15/15 when rerun alone.server-functions-adapter-requestone, which also fails on next.served-error-no-reask› "[streamed] the content waits for its sheet". That test is flaky onnexttoo, and the failing scenario has only a<Loading>boundary, which the new check never touches. With next's ownboundaries.tsrebuilt 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 vsnext; brotli moves by ±60 B of compression noise. Hello world and the frozen core floors are unchanged.Generated by Claude Code