dabeaz / curio

Good Curio!
Other
4.01k stars 240 forks source link

Exceptions happening in TaskGroup won't get caught #314

Closed ArneGueldener closed 4 years ago

ArneGueldener commented 4 years ago

Hi,

I want to update from curio 0.9 to 1.0 I have got the problem that exceptions which happened in TaskGroups won't get caught anymore (works in 0.9 though).

import curio

async def my_exception_raiser():
    raise Exception("test")

async def main():
    try:
        async with curio.TaskGroup() as g:
            await g.spawn(my_exception_raiser)
    except Exception:
        print("\nException, where are you?\n")
        raise
    print("I shouldn't come here")

curio.run(main)
dabeaz commented 4 years ago

There has been a major refactoring/simplification of exception handling with Task groups. More detail is in the CHANGES file (and also the docs). But, you'd need to change the above code to something like this:

import curio

async def my_exception_raiser():
    raise Exception("test")

async def main():
    async with curio.TaskGroup() as g:
        await g.spawn(my_exception_raiser)
    if g.exception:
        print("It failed!")
    else:
        print("I shouldn't come here")

curio.run(main)
ArneGueldener commented 4 years ago

Okay, thank you. I noticed a further thing: If no task will be spawned, I get an AttributeError that there is no member "exception" for the TaskGroup. I don't know if that is on purpose.

import curio

async def handle_list_item(item):
    print(f"Got {item}")

async def main():
    my_list = []  # 1,2,3,4,5,
    async with curio.TaskGroup() as g:
        for item in my_list:
            await g.spawn(handle_list_item, item)
    if g.exception:  # Raises attribute error
        print("It failed")

curio.run(main)
dabeaz commented 4 years ago

The g.exception (and g.result) attributes depend on the completion of a task. If there's a possibility of no tasks, add an additional check for g.completed. For example:

async with curio.TaskGroup() as g:
    ...
if g.completed and g.exception:
    print("It failed")

I'd need to think more about whether I'd want g.exception to work in this case as having no exceptions is slightly different than not doing anything at all.

dabeaz commented 4 years ago

After some further pondering, I think I'm ok with having g.exception return None even no tasks are spawned. On the other hand, accessing g.result will still raise an exception (as getting a result assumes that some task actually completed successfully).

ArneGueldener commented 4 years ago

Thank you for your help. I wrote a small wrapper to get the old behaviour done. For others who are interested or want to improve that:

class TaskGroupWrapper(curio.TaskGroup):
      async def __aexit__(self, ty, val, tb):
        result = await super().__aexit__(ty, val, tb)
        if not val:
            list_exceptions = [exception for exception in self.exceptions if exception is not None]
            if list_exceptions:
                if len(list_exceptions) > 1:
                    print(f"There are more than 1 exception {len(list_exceptions)}\n Only raising first")
                raise list_exceptions[0]
        return result