user3599803
user3599803

Reputation: 7034

python asyncio exceptions raised from loop.create_task()

I want my code to use python logging to log exceptions. In my usual code using await, exceptions are raised normally, so:

try:
   await coro()
except Exception as e:
   logger.exception("Exception happened")

...works fine.

However, when using

loop.create_task(coro())

....I'm not sure how can I catch the exception here. Wrapping the create_task() call with a try/except obviously won't work. What is the best solution to log every exception in the code?

Upvotes: 14

Views: 11688

Answers (5)

gelonida
gelonida

Reputation: 5640

If you want to react to the exception of a task as soon as it occurs you can use add_done_callback() ( https://docs.python.org/3/library/asyncio-future.html#asyncio.Future.add_done_callback )

asyncio.Task objects is an asyncio.Future like object and has the add_done_callback() method.

In the callback function you just have to get the result() of the future to provoke an exception. with try except you can add custom handling / logging whatever.

import asyncio

async def sleepError(x):
  await asyncio.sleep(x)
  print(1)
  throw_error = 1 / 0

def done_callback(futr):
    try:
        rslt = futr.result()
    except Exception as exc:
        # do_something_with(exc) if you want to (like logging)
        # or just raise
        raise

async def sleepOk(x):
  await asyncio.sleep(x)
  print(2)

async def main():
  x = asyncio.create_task(sleepError(1))
  # without next line exception will only occur at end of main
  x.add_done_callback(done_callback)
  await sleepOk(2)
  print(3)

asyncio.run(main())

If you just want to see the exception on the console, then following callback is sufficient:

def done_callback(futr):
    rslt = futr.result()

Upvotes: 0

Marquinho Peli
Marquinho Peli

Reputation: 5135

The proper way is to use create_task but you need await it if you want to catch the exception at some point:

import asyncio

async def sleepError(x):
  await asyncio.sleep(x)
  print(1)
  throw_error = 1 / 0

async def sleepOk(x):
  await asyncio.sleep(x)
  print(2)

async def main():
  x = asyncio.create_task(sleepError(1))
  await sleepOk(2)
  
  """
  await x
  # print(3) bellow works without "await x", and print(1) from sleepError as well
  # You can try/except the "await x" line
  # if you "await x" without try/except, print(3) is not executed but error happens.
  # if you don't "await x" you get warning: Task exception was never retrieved
  """
  print(3)


asyncio.run(main())

Upvotes: 0

AnT
AnT

Reputation: 891

Expanding on @user4815162342's solution, I created a wrapper around log_exceptions to avoid having to nest every coroutine inside 2 functions:

import asyncio
from typing import Awaitable

def create_task_log_exception(awaitable: Awaitable) -> asyncio.Task:
    async def _log_exception(awaitable):
        try:
            return await awaitable
        except Exception as e:
            logger.exception(e)
    return asyncio.create_task(_log_exception(awaitable))

Usage:

create_task_log_exception(coroutine())

Upvotes: 3

user4815162342
user4815162342

Reputation: 155296

What is the best solution to log every exception in the code?

If you control the invocation of create_task, but don't control the code in the coro(), then you can write a logging wrapper:

async def log_exceptions(awaitable):
    try:
        return await awaitable
    except Exception:
        logger.exception("Unhandled exception")

then you can call loop.create_task(log_exceptions(coro())).

If you can't or don't want to wrap every create_task, you can call loop.set_exception_handler, setting the exception to your own function that will log the exception as you see fit.

Upvotes: 10

shmee
shmee

Reputation: 5101

Just so that it has been mentioned: asyncio.Task objects have the methods result and exception.
result:

[...] if the coroutine raised an exception, that exception is re-raised [...]

exception:

[...] If the wrapped coroutine raised an exception that exception is returned [...]

Given a simple setup (in Python 3.7 syntax):

import asyncio
tasks =[]

async def bad_test():
    raise ValueError

async def good_test():
    return

async def main():
    tasks.append(asyncio.create_task(bad_test()))
    tasks.append(asyncio.create_task(good_test()))

asyncio.run(main())

Using result, one could do:

for t in tasks:
    try:
        f = t.result()
    except ValueError as e:
        logger.exception("we're all doomed")

Or, using exception:

for t in tasks:
    if isinstance(t.exception(), Exception):
        logger.exception("apocalypse now")

However, both methods require the Task to be done, otherwise:

If the Task has been cancelled, this method raises a CancelledError exception.

(result): If the Task’s result isn’t yet available, this method raises a InvalidStateError exception.

(exception): If the Task isn’t done yet, this method raises an InvalidStateError exception.

So, unlike the proposal in the other answer, the logging will not happen when the exceptions raise in the tasks, but rather when the tasks are evaluated after they completed.

Upvotes: 4

Related Questions