Managing unhandled exceptions from asyncio

I think that asyncio should provide an asyncio exception_handler that raises unhandled asyncio exceptions out of run_forever in a BaseExceptionGroup (and enable it by default in some future release, deprecating and removing loop.set_exception_handler)

There may need to be some tweaks in run_until_complete that catches this exception, cancels the root task, and resumes run_forever until the task has finished, merging any unhandled exception ExceptionGroups

This is the default behaviour of trio, and the anyio pytest plugin provides this functionality - failing any test with an exception group if there’s any unhandled exceptions.

1 Like

I don’t think this should become the default behavior in asyncio. I would be broadly in favor of a runner subclass that can be used that has this behavior for those who want it, and for that runner subclass to be in asyncio, not just a documented recipe or a third-party library addition, but changing the semantics of unhandled exceptions in asyncio tasks by default would be extremely breaking, and I don’t see the benefit of it.

There are many frameworks that launch tasks using user-defined callbacks. These frameworks should not need to then also manually catch and suppress exceptions in user code to prevent a faulty route handler or edge case in a route handler from becoming a denial of service vector against the entire service, they should be able to rely on the existing semantics for what will and will not result in the root task raising.

these frameworks would just need to run the user defined tasks in a TaskGroup, which would automatically collect and route errors to the correct location

I don’t think this makes sense as a runner subclass - it makes sense as an option to set on the loop and pass to Runner’s loop_factory

Task groups have their own issues, and aren’t appropriate for use in the same places. Libraries can’t use task groups without forcing the use of context managers where they otherwise aren’t needed or having application code pass in a task group.

Either way, by suggesting this become the default behavior, you’re suggesting breaking existing working code and forcing people to change how it is written for an opinionated view that this is better.

This shouldn’t even be an option in asyncio. Library code will be broken by this option existing and will have to assume users might set it.

I don’t think the setting itself would be a problem. I do think making it the default behavior would be. Users can already get this behavior in asyncio in a few ways if they want it with exposed asyncio configuration, and libraries can rightfully turn around and tell the user that setting that up to kill their entrypoint was their own decision. It’s much more of a problem if this opinionated choice is done by python, changing the semantics that are expected.

In my previous article (Server-oriented task scope design - #2 by achimnol), I’ve suggested to have a nested tree of “task scope” (or “supervisor scope”) for an asyncio server application. One of the core idea is to be able to consume the unhandled exceptions in a hierarchical manner, instead of relying on the global loop.set_exception_handler().

Without impacting existing asyncio applications, I think we could reconsider the concept of nested task scopes.

I don’t think it’s a good idea to add the additional overhead you’ve described in the linked post in general, and don’t see how it’s relevant to this thread.