This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: Inner exception is not being raised using asyncio.gather
Type: behavior Stage: resolved
Components: asyncio Versions: Python 3.7
process
Status: closed Resolution: not a bug
Dependencies: Superseder:
Assigned To: Nosy List: Drew Budwin, asvetlov, iritkatriel, twisteroid ambassador, yselivanov
Priority: normal Keywords:

Created on 2019-04-15 17:52 by Drew Budwin, last changed 2022-04-11 14:59 by admin. This issue is now closed.

Messages (3)
msg340297 - (view) Author: Drew Budwin (Drew Budwin) Date: 2019-04-15 17:52
Using Python 3.7, I am trying to catch an exception and re-raise it by following an example I found on StackOverflow (https://stackoverflow.com/a/6246394/1595510). While the example does work, it doesn't seem to work for all situations. Below I have two asynchronous Python scripts that try to re-raise exceptions. The first example works, it will print both the inner and outer exception.

import asyncio

class Foo:
    async def throw_exception(self):
        raise Exception("This is the inner exception")

    async def do_the_thing(self):
        try:
            await self.throw_exception()
        except Exception as e:
            raise Exception("This is the outer exception") from e

async def run():
    await Foo().do_the_thing()

def main():
    loop = asyncio.get_event_loop()
    loop.run_until_complete(run())

if __name__ == "__main__":
    main()

Running this will correctly output the following exception stack trace:

$ py test.py
Traceback (most recent call last):
  File "test.py", line 9, in do_the_thing
    await self.throw_exception()
  File "test.py", line 5, in throw_exception
    raise Exception("This is the inner exception")
Exception: This is the inner exception

The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  File "test.py", line 21, in <module>
    main()
  File "test.py", line 18, in main
    loop.run_until_complete(run())
  File "C:\Python37\lib\asyncio\base_events.py", line 584, in run_until_complete
    return future.result()
  File "test.py", line 14, in run
    await Foo().do_the_thing()
  File "test.py", line 11, in do_the_thing
    raise Exception("This is the outer exception") from e
Exception: This is the outer exception

However, in my next Python script, I have multiple tasks that I queue up that I want to get a similar exception stack trace from. Essentially, I except the above stack trace to be printed 3 times (once for each task in the following script). The only difference between the above and below scripts is the run() function.

import asyncio

class Foo:
    async def throw_exception(self):
        raise Exception("This is the inner exception")

    async def do_the_thing(self):
        try:
            await self.throw_exception()
        except Exception as e:
            raise Exception("This is the outer exception") from e

async def run():
    tasks = []

    foo = Foo()

    tasks.append(asyncio.create_task(foo.do_the_thing()))
    tasks.append(asyncio.create_task(foo.do_the_thing()))
    tasks.append(asyncio.create_task(foo.do_the_thing()))

    results = await asyncio.gather(*tasks, return_exceptions=True)

    for result in results:
        if isinstance(result, Exception):
            print(f"Unexpected exception: {result}")

def main():
    loop = asyncio.get_event_loop()
    loop.run_until_complete(run())

if __name__ == "__main__":
    main()

The above code snippet produces the disappointingly short exceptions lacking stack traces.

$ py test.py
Unexpected exception: This is the outer exception
Unexpected exception: This is the outer exception
Unexpected exception: This is the outer exception

If I change return_exceptions to be False, I will get the exceptions and stack trace printed out once and then execution stops and the remaining two tasks are cancelled. The output is identical to the output from the first script. The downside of this approach is, I want to continue processing tasks even when exceptions are encountered and then display all the exceptions at the end when all the tasks are completed.
msg340311 - (view) Author: twisteroid ambassador (twisteroid ambassador) * Date: 2019-04-16 05:53
The difference is because you grabbed and print()ed the exception themselves in Script 2, while in Script 1 you let Python's built-in unhandled exception handler (sys.excepthook) print the traceback for you.

If you want a traceback, then you need to print it yourself. Try something along the lines of this:

    traceback.print_tb(result.__traceback__)

or:

    traceback.print_exception(type(result), result, result.__traceback__)

or if you use the logging module:

    logging.error('Unexpected exception', exc_info=result)


reference: https://stackoverflow.com/questions/11414894/extract-traceback-info-from-an-exception-object
msg387478 - (view) Author: Irit Katriel (iritkatriel) * (Python committer) Date: 2021-02-21 23:24
"twisteroid ambassador"'s explanation is correct - you need to use the traceback module to render complete information about an exception.

And indeed, return_exceptions=False gives you the first exception (not all of them) as explained here https://docs.python.org/3/library/asyncio-task.html#asyncio.gather
History
Date User Action Args
2022-04-11 14:59:14adminsetgithub: 80817
2021-02-21 23:24:43iritkatrielsetstatus: open -> closed

nosy: + iritkatriel
messages: + msg387478

resolution: not a bug
stage: resolved
2019-04-16 05:53:36twisteroid ambassadorsetnosy: + twisteroid ambassador
messages: + msg340311
2019-04-15 17:52:43Drew Budwincreate