Repository navigation
fix(signals): route async protocol throws and error-formatting failures to the memo (#4112) - #4158
Conversation
… the memo (#4112) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
🦋 Changeset detectedLatest commit: d17154e 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)
|
Coverage Report for CI Build 39331327442Coverage remained the same at 77.555%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will improve performance by 31.92%
|
| 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)
Footnotes
-
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
Summary
Fixes #4112.
Before: three kinds of failure escaped the memo's error handling.
next()(or a step'sthen) of an async iterator became an unhandled rejection, and the memo kept its last value.[Symbol.asyncIterator]getter or factory, or the firstnext()after a thenable resolved to an iterable) left the memo stuck inNotReadyError.StatusErrorformatting a null-prototype object, or an error with a throwingmessagegetter, threw aTypeErrorout ofcreateMemothat 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:
consumeIterator'siterate()(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 callshandleError. The non-initial branch also gets thesettleAutodispose()the async rejection handler already has.flattenIfIterableand callshandleErrorplussettleAutodisposeon 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 keepscause: original.When a protocol call throws, the iterator is treated as completed and
return()is not called. That matchesfor await.How did you test this change?
packages/signals/tests/async-protocol-throw-4112.test.ts, 7 cases: one control (a rejectednext()reaches the boundary), four async escape cases checked through acreateErrorBoundaryfallback with no unhandled rejections allowed, and two formatting cases. Without the fix, 6 fail and the control passes. With it, all 7 pass.packages/signalsfull 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