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)

Most upvoted comments

It would be nice if you could update the documentation though. Right now ExceptionGroup seems to be gone from the documentation entirely?

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.