anyio: `BaseException` not returned though `blockingportal.start_task_soon(...).result()`

Take this code:

async def test() -> None:
    raise KeyboardInterrupt

    # Instead of the above raise statement, we can use prompt_toolkit to read
    # input. Pressing control-c raises a `KeyboardInterrupt` here. (That's not
    # the default SIGINT handler, which would be handled in the main thread.)
    from prompt_toolkit import PromptSession  # pip install prompt-toolkit

    await PromptSession().prompt_async(">")

    # Instead of `KeyboardInterrupt`, raising `BaseException` yields similar
    # results.
    raise BaseException("test")

def main() -> None:
    with start_blocking_portal() as portal:

        # We never get here:
        # - Either the code just hangs,
        # - the portal gets killed

if __name__ == "__main__":

The outcome of this is very nondeterministic.

Sometimes it hangs. Sometimes we get a short traceback. Sometimes a very long traceback. Usually the portal gets killed and becomes unusable.

What I’d expect is that any exception raised by our test function is returned through the concurrent.futures.Future, not killing the portal. A KeyboardInterrupt is a valid outcome of an async function and should not kill the portal.

What worries me most is that it’s nondeterministic, and that it sometimes hangs, as if there is a race condition.

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 17 (12 by maintainers)

Commits related to this issue

Most upvoted comments

Yes, I’d expect any exception to be propagated through the concurrent.futures.Future because that’s the place where it can/should be handled.

I’d expect an Exception to be propagated into the Future, yes. But not a BaseException. The latter should always be re-raised (except for catching cancellations obviously); if there’s a Future to be signalled along the way, fine, but please feed a new Exception-derived object to it. Not the original.