Fix an unhandled exception in an async rx operation - #1179
Open
philippjfr wants to merge 2 commits into
Open
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1179 +/- ##
=======================================
Coverage 86.90% 86.90%
=======================================
Files 9 9
Lines 5398 5406 +8
=======================================
+ Hits 4691 4698 +7
- Misses 707 708 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
hoxbro
reviewed
Aug 28, 2026
|
|
||
| irx.rx.value = 2 | ||
| async_rx.rx.value | ||
| # Yield to the event loop so the failing task is suspended on its await and |
Member
Author
There was a problem hiding this comment.
It's the correct word imo.
Member
There was a problem hiding this comment.
It is just so close to generators that it can have double meaning.
Co-authored-by: Simon Høxbro Hansen <hoxbro@protonmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An exception raised inside an async operation is never surfaced to the reader. The node reports
_awaiting == Trueforever, every read returns the stale value, and the only trace is an asyncio"Task exception was never retrieved" warning on stderr.
The synchronous path does the right thing:
_resolvestores the exception in_error_stateandre-raises it on every read until an invalidation clears it, so a failing operation reports its
failure and a new input recovers the pipeline. The async path has neither property.
Cause
_resolve_asynccatches onlyasyncio.CancelledError. Anything else escapes the task, so none ofthe three publish paths (shared adoption, async generator, awaited coroutine) ever runs its tail:
_finished_generationis not advanced,_error_stateis not set, and no trigger fires._awaitingis defined as_resolve_generation != _finished_generation, so leaving the finishedgeneration behind is what strands the node. Since #1173 that also makes the node report itself as
skipped on every read, which is why the stale value stops propagating but nothing replaces it.
Fix
One new handler in
_resolve_async(param/reactive.py:1876), in order:resolution owns the state and the abandoned computation's error is irrelevant.
_finished_generationis advanced either way, so_awaitingsettles.case matters because resolution is demand-driven:
_resolve_generationonly bumps whensomething reads the node, so a task can raise after a new input arrived and before anyone
read the node again. Recording the error there would poison the node permanently, because only
an invalidation clears
_error_stateand reads raise before ever reaching_lazy_resolve.Dropping it is safe: the next read resolves the new input and supersedes the failed one.
_resolve. Every readre-raises until an invalidation clears
_error_state, and a new input recovers the pipeline.For an async generator the raise ends the stream.
The exception is no longer re-raised out of the task, so the raise itself no longer produces a
"Task exception was never retrieved" warning. With a watcher attached, the trigger invokes it, the
watcher reads the value and raises, and that propagates out of the task to the loop's exception
handler. This is the async analogue of the synchronous path raising at the mutation point, and it
matches what already happens today when a watcher raises on a successful async resolution.
Errors reach downstream and branching consumers without extra work: a downstream node's
_prev._resolve()raises and is recorded by the existingexcept Exceptionin_resolve, and each_sharedmirror records the error when it adopts the shared node's value.The
trigger is Noneguard also moved above the_current_taskclaim. It was the first statementinside the
trywith noawaitbetween, so this is behavior-identical, and it keeps the typenarrowing valid inside the new handler.