anyio: Surprising behavior because of ExceptionGroup
Hi,
Imagine the following code:
import anyio
async def task1():
# This task raises an exception. Oops.
raise ValueError
async def task2():
raise ValueError # Another bug
async def run_tasks():
async with anyio.create_task_group() as tg:
await tg.spawn(task1)
await tg.spawn(task2)
async def main():
try:
await run_tasks()
except Exception:
# Catch any exception that might occur in a child task
print("Something went wrong")
anyio.run(main)
The ValueErrors
aren’t caught by main
, because the task group merges them into an ExceptionGroup
, which is not a subclass of Exception
. Perhaps this is the intended behavior, but it did lead to a surprise on my end. The tasks only raise ValueError
after all, which would normally be caught by the try-except statement.
To provide context: I’m writing a server and want to make sure that exceptions that are raised by a single connection do not affect the other connections. Whenever an exception occurs, the server should only close the connection that caused the exception, but keep serving other connections.
In my case it was even more complicated:
import anyio
async def task1():
# This task raises an exception. Oops.
raise ValueError
async def task2():
await anyio.sleep(1) # No bugs here
async def run_tasks():
try:
async with anyio.create_task_group() as tg:
await tg.spawn(task1)
await tg.spawn(task2)
except Exception:
# Catch any exception that might occur in a child task
print("Something went wrong")
async def main():
async with anyio.create_task_group() as tg:
await tg.spawn(run_tasks)
await tg.cancel_scope.cancel()
anyio.run(main)
This script does not even terminate with an ExceptionGroup
. It terminates with a ValueError
, even though the code that raises the ValueError
is within the try-except block. I was really confused when this happened. It felt like the ValueError
was leaking through the try-except statement somehow.
With a bit of debugging, I eventually figured out what’s happening. When main
cancels run_tasks
it implicitly raises a CancelledError
in task2
. The task group in run_tasks
merges both exceptions into an ExceptionGroup
. Because ExceptionGroup
is not a subclass of Exception
it is not caught by the try-except statement. When the ExceptionGroup
leaves the task group in main
, the CancelledError
is filtered away. Only the ValueError
remains.
I don’t want to catch BaseException
here, because I don’t want to catch exceptions like CancelledError
or KeyboardInterrupt
. I also can’t catch ExceptionGroup
here, for the same reason. Anyio’s documentation does not seem to explain any way to extract a specific exception from an exception group, so I’m not sure how I should deal with this.
What do you think?
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 1
- Comments: 15 (9 by maintainers)
The exceptions were missing entirely from the documentation in v1.X which you are probably viewing right now (stable). Check out the
latest
in the docs viewer and you’ll see it. I will add this in the docs and close the issue then.