question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Surprising behavior because of ExceptionGroup

See original GitHub issue

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?

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:1
  • Comments:15 (9 by maintainers)

github_iconTop GitHub Comments

1reaction
agronholmcommented, Aug 5, 2020

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.

0reactions
kinnaycommented, Aug 7, 2020

Sure, sorry about that.

Read more comments on GitHub >

github_iconTop Results From Across the Web

How Exception Groups Will Improve Error Handling in AsyncIO
PEP 654, Exception Groups, is a freshly accepted new feature for the upcoming Python 3.11 edition. In this talk we'll cover why it's ......
Read more >
except* fails on unhashable exceptions - documented ...
I think when classes inherit default behavior from object , the question of whether that behavior is part of their "contract" comes down...
Read more >
Flat exception groups (alternative to PEP 654)
If you do have an ExceptionGroup([ConnectionResetError, FileNotFoundError]) , then this behavior is surprising, but every other behavior would be even more ...
Read more >
[Python-Dev] Re: PEP 654: Exception Groups and except* [REPOST]
Hi Paul, > > IIUC, you are saying that exception group should not be a builtin type > because it is (1) complex...
Read more >
Python 3.11 Preview: Task and Exception Groups - Real Python
This behavior is particularly useful if you happen to have an issue in your error handling code, because you then get information about...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found