You can log exceptions in asyncio programs by calling logging.exception() within tasks or when awaiting tasks.
We can also configure the event loop to automatically log never-retrieved exceptions when the program is shut down.
In this tutorial, you will discover how to log exceptions from asyncio.
Let’s get started.
Need to Log Exceptions in Asyncio
Asyncio programs may raise exceptions.
This may be by design as part of the normal operation of the program. It may also raise exceptions for unexpected situations.
In both cases, it is good practice to log exceptions raised in an asyncio program.
This is essential for many reasons, such as:
- Error Diagnosis: Logging exceptions helps diagnose issues and errors in asyncio code. It provides details about what went wrong, including error messages and stack traces, making it easier to pinpoint the root cause of problems.
- Debugging: In asynchronous code, tracing the flow of execution can be challenging. Logging exceptions at critical points in your asyncio code allows you to understand how and where exceptions are raised, helping you identify and resolve bugs.
- Visibility: Exceptions logged to a central location, such as a file or a centralized logging service, provide visibility into the health of your asyncio application. Monitoring these logs allows you to proactively address issues before they affect users.
- Production Debugging: In production environments, where debugging interactively is limited, logs are often the primary source of information for diagnosing problems. Exception logs provide a historical record of errors and exceptions that occurred during the application’s operation.
- Error Reporting: Exception logs serve as a reporting mechanism for errors that may require immediate attention. They can trigger notifications or alerts to alert developers or system administrators when critical errors occur.
Logging exceptions in asyncio programs is crucial for maintaining the reliability and stability of asynchronous applications.
How can we log exceptions in asyncio programs?
Run loops using all CPUs, download your FREE book to learn how.
How to Log Exceptions in Asyncio
There are perhaps two ways to log exceptions in asyncio programs, they are:
- Manually Logging Exceptions
- Automatically Logging Exceptions
Let’s take a closer look at each in turn.
How to Log Exceptions Manually
We can manually log an exception in an asyncio task or coroutine.
This can be achieved by wrapping the offending code in a try-except block and calling the logging.exception() method.
Logs a message with level ERROR on this logger.
— logging — Logging facility for Python
For example:
1 2 3 4 5 6 |
... try: ... except Exception as e: # log the exception logging.exception(f'Failed with exception: {e}') |
It is common for tasks to handle a CancelledError in order to close resources and then propagate the raised CancelledError exception to the caller.
You can learn more about best practices for canceling tasks in the tutorial:
We may want to add debug logging to task cancellation to provide insight into task cancellation.
This can be achieved by logging CancelledError exceptions via logging.debug().
Logs a message with level DEBUG on this logger.
— logging — Logging facility for Python
For example:
1 2 3 4 5 6 7 8 9 10 |
... try: ... except CancelledError: # shutdown resources ... # log the cancellation logging.debug(f'Task was cancelled, details: {...}') # re-raise the exception raise |
How to Log Never-Retrieved Exceptions Automatically
In asyncio, if a background task fails with an unhandled exception and the exception is not retrieved, this is called a “never-retrieved” exception.
They are also called “asyncio silent exceptions”, as they are not reported at the time they occur.
The asyncio event loop will log these never-retrieved exceptions by default when the tasks are garbage-collected, such as when the asyncio program is terminated.
If a Future.set_exception() is called but the Future object is never awaited on, the exception would never be propagated to the user code. In this case, asyncio would emit a log message when the Future object is garbage collected.
— Developing with asyncio
The asyncio event loop provides a handler that is called automatically when reporting never-retrieved exceptions.
This is achieved by getting an instance of the event loop and calling the set_exception_handler() method to configure a handler function to call when never-retrieved exceptions are logged.
We can develop a handler function that logs never-retrieved exceptions.
Generally, we should only call the logging.exception() function from an except block, otherwise we might get unexpected behavior (e.g. the infrastructure expects an exception to have been raised).
This function should only be called from an exception handler.
— logging — Logging facility for Python
Therefore, when logging exceptions from an asyncio exception handler, we need to use a different level, e.g. error().
For example:
1 2 3 4 5 6 |
# define an exception handler def exception_handler(loop, context): # get the exception object exp = context['exception'] # log exception logging.error(f'Never-retrieved exception: {exp}') |
We can then register our custom exception handler as a first step in our asyncio program.
For example:
1 2 3 4 5 |
... # get the event loop loop = asyncio.get_running_loop() # set the exception handler loop.set_exception_handler(exception_handler) |
You can learn more about asyncio silent exceptions in the tutorial:
Now that we know how to log exceptions in asyncio programs, let’s look at some worked examples.
Example of Logging Asyncio Task Exceptions From Within
We can explore an example of logging asyncio exceptions from within a task.
In this example, we will define a task that will conditionally raise an exception. We will log normal operations and catch any raised exceptions and log them. The main coroutine will prepare the logging infrastructure and create many tasks, then wait for them to be completed.
Firstly, we can define the task.
The task takes an integer argument value. It reports a starting message and sleeps for a moment. We then check its integer argument. If it is divisible by two with no remainder (e.g. even), an exception is raised and then handled and logged. Otherwise, a done message is logged.
The task() coroutine below implements this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# task that does work and logs async def task(value): try: # log a message logging.info(f'Task {value} is starting') # simulate doing work await asyncio.sleep(1) # check if it should fail if value % 2 == 0: raise Exception(f'Task {value} failed') # log a message logging.info(f'Task {value} is done') except Exception as e: # log the exception logging.exception(f'Failed with exception: {e}') |
Next, we can define the main() coroutine that will first configure the logger to log all messages with a DEBUG level and above.
It will then log a starting message, create a TaskGroup, and start 10 versions of our task() coroutine with arguments from 0 to 9. It then waits for all tasks to complete then logs a final done message.
The main() coroutine below implements this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# main coroutine async def main(): # get the root logger log = logging.getLogger() # log all messages, debug and up log.setLevel(logging.DEBUG) # log a message logging.info(f'Main is starting') # issue many tasks async with asyncio.TaskGroup() as group: for i in range(10): _ = group.create_task(task(i)) # log a message logging.info(f'Main is done') |
Finally, we can start the asyncio event loop.
1 2 3 |
... # start the event loop asyncio.run(main()) |
Tying this together, the complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# SuperFastPython.com # example of manually logging exceptions in asyncio import logging import asyncio # task that does work and logs async def task(value): try: # log a message logging.info(f'Task {value} is starting') # simulate doing work await asyncio.sleep(1) # check if it should fail if value % 2 == 0: raise Exception(f'Task {value} failed') # log a message logging.info(f'Task {value} is done') except Exception as e: # log the exception logging.exception(f'Failed with exception: {e}') # main coroutine async def main(): # get the root logger log = logging.getLogger() # log all messages, debug and up log.setLevel(logging.DEBUG) # log a message logging.info(f'Main is starting') # issue many tasks async with asyncio.TaskGroup() as group: for i in range(10): _ = group.create_task(task(i)) # log a message logging.info(f'Main is done') # start the event loop asyncio.run(main()) |
Running the program first starts the event loop and runs the main() coroutine.
The main() coroutine runs and sets the logging level, then logs an initial info message.
It then creates a TaskGroup and schedules 10 task() coroutines, then blocks until they are done.
The tasks run, first logging a message and then suspending with a sleep.
The tasks resume and check their argument value. If it is even, an exception is raised, caught and logged, otherwise an info message is logged.
Once all tasks are done, the main() coroutine resumes and logs a final info message.
This highlights how we can log exceptions directly from within our asyncio tasks.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
INFO:root:Main is starting INFO:root:Task 0 is starting INFO:root:Task 1 is starting INFO:root:Task 2 is starting INFO:root:Task 3 is starting INFO:root:Task 4 is starting INFO:root:Task 5 is starting INFO:root:Task 6 is starting INFO:root:Task 7 is starting INFO:root:Task 8 is starting INFO:root:Task 9 is starting ERROR:root:Failed with exception: Task 0 failed Traceback (most recent call last): File "...", line 15, in task raise Exception(f'Task {value} failed') Exception: Task 0 failed INFO:root:Task 1 is done ERROR:root:Failed with exception: Task 2 failed Traceback (most recent call last): File "...", line 15, in task raise Exception(f'Task {value} failed') Exception: Task 2 failed INFO:root:Task 3 is done ERROR:root:Failed with exception: Task 4 failed Traceback (most recent call last): File "...", line 15, in task raise Exception(f'Task {value} failed') Exception: Task 4 failed INFO:root:Task 5 is done ERROR:root:Failed with exception: Task 6 failed Traceback (most recent call last): File "...", line 15, in task raise Exception(f'Task {value} failed') Exception: Task 6 failed INFO:root:Task 7 is done ERROR:root:Failed with exception: Task 8 failed Traceback (most recent call last): File "...", line 15, in task raise Exception(f'Task {value} failed') Exception: Task 8 failed INFO:root:Task 9 is done INFO:root:Main is done |
Next, let’s look at how we might log exceptions from tasks that fail while being awaited.
Free Python Asyncio Course
Download your FREE Asyncio PDF cheat sheet and get BONUS access to my free 7-day crash course on the Asyncio API.
Discover how to use the Python asyncio module including how to define, create, and run new coroutines and how to use non-blocking I/O.
Example of Logging Awaited Task Exceptions
We can explore an example of logging tasks that raise an exception while being awaited.
In this case, we can update the above example and remove the exception handling from the task() coroutine.
1 2 3 4 5 6 7 8 9 10 11 |
# task that does work and logs async def task(value): # log a message logging.info(f'Task {value} is starting') # simulate doing work await asyncio.sleep(1) # check if it should fail if value % 2 == 0: raise Exception(f'Task {value} failed') # log a message logging.info(f'Task {value} is done') |
We then update the main() to no longer use a TaskGroup and instead issue all tasks and keep the returned asyncio.Task objects. This can be done in a list comprehension.
1 2 3 |
... # schedule all tasks tasks = [asyncio.create_task(task(i)) for i in range(10)] |
We can then loop over each task and await each in turn.
If an exception is raised, we can handle and log it directly without disrupting the loop.
1 2 3 4 5 6 7 8 |
... # await each task and handle exceptions for t in tasks: try: await t except Exception as e: # log the exception logging.exception(f'Task failed with exception: {e}') |
Tying this together, the complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
# SuperFastPython.com # example of logging task exceptions in asyncio import logging import asyncio # task that does work and logs async def task(value): # log a message logging.info(f'Task {value} is starting') # simulate doing work await asyncio.sleep(1) # check if it should fail if value % 2 == 0: raise Exception(f'Task {value} failed') # log a message logging.info(f'Task {value} is done') # main coroutine async def main(): # get the root logger log = logging.getLogger() # log all messages, debug and up log.setLevel(logging.DEBUG) # log a message logging.info(f'Main is starting') # schedule all tasks tasks = [asyncio.create_task(task(i)) for i in range(10)] # await each task and handle exceptions for t in tasks: try: await t except Exception as e: # log the exception logging.exception(f'Task failed with exception: {e}') # log a message logging.info(f'Main is done') # start the event loop asyncio.run(main()) |
Running the program first starts the event loop and runs the main() coroutine.
The main() coroutine runs and sets the logging level, then logs an initial info message.
It then schedules all 10 tasks and stores the Task objects in a list.
The main() coroutine then iterates over the tasks and awaits the first one.
The tasks run, first logging a message and then suspending with a sleep.
The tasks resume and check their argument value. If it is even, an exception is raised, terminating the task, otherwise, an info message is logged.
The main() coroutine resumes. Some tasks finish normally, while others fail with an exception, which is logged directly.
This highlights how we can log exceptions from awaited tasks that fail.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
INFO:root:Main is starting INFO:root:Task 0 is starting INFO:root:Task 1 is starting INFO:root:Task 2 is starting INFO:root:Task 3 is starting INFO:root:Task 4 is starting INFO:root:Task 5 is starting INFO:root:Task 6 is starting INFO:root:Task 7 is starting INFO:root:Task 8 is starting INFO:root:Task 9 is starting INFO:root:Task 1 is done INFO:root:Task 3 is done INFO:root:Task 5 is done INFO:root:Task 7 is done INFO:root:Task 9 is done ERROR:root:Task failed with exception: Task 0 failed Traceback (most recent call last): File "...", line 31, in main await t File "...", line 14, in task raise Exception(f'Task {value} failed') Exception: Task 0 failed ERROR:root:Task failed with exception: Task 2 failed Traceback (most recent call last): File "...", line 31, in main await t File "...", line 14, in task raise Exception(f'Task {value} failed') Exception: Task 2 failed ERROR:root:Task failed with exception: Task 4 failed Traceback (most recent call last): File "...", line 31, in main await t File "...", line 14, in task raise Exception(f'Task {value} failed') Exception: Task 4 failed ERROR:root:Task failed with exception: Task 6 failed Traceback (most recent call last): File "...", line 31, in main await t File "...", line 14, in task raise Exception(f'Task {value} failed') Exception: Task 6 failed ERROR:root:Task failed with exception: Task 8 failed Traceback (most recent call last): File "...", line 31, in main await t File "...", line 14, in task raise Exception(f'Task {value} failed') Exception: Task 8 failed INFO:root:Main is done |
Next, we can look at logging canceled tasks that raise a CancelledError exception.
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Example of Logging CancelledError in Asyncio
We can explore an example of logging a CancelledError exception when a task is canceled.
In this case, we can update the above example to catch a CancelledError exception, log it, and then re-raise it to allow the caller to handle the exception, if it desires (a good practice).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# task that does work and logs async def task(value): try: # log a message logging.info(f'Task {value} is starting') # simulate doing work await asyncio.sleep(1) # log a message logging.info(f'Task {value} is done') except asyncio.CancelledError: # log the cancellation logging.debug(f'Task {value} cancelled') # re-raise the exception raise |
You can learn more about best practices when canceling tasks in the tutorial:
The main() coroutine can then be updated to create 10 tasks using a list comprehension as before, then wait a moment before canceling the first 3 tasks.
This will cause the tasks to raise a CancelledError exception, which will be logged and re-raised, before terminating the task.
1 2 3 4 5 6 7 8 9 10 11 |
... # schedule many tasks tasks = [asyncio.create_task(task(i)) for i in range(10)] # let them run a moment await asyncio.sleep(0.5) # cancel some tasks tasks[0].cancel() tasks[1].cancel() tasks[2].cancel() # let things finish up await asyncio.sleep(0.5) |
You can learn more about canceling tasks in the tutorial:
Tying this together, the complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# SuperFastPython.com # example of manually logging CancelledError exceptions import logging import asyncio # task that does work and logs async def task(value): try: # log a message logging.info(f'Task {value} is starting') # simulate doing work await asyncio.sleep(1) # log a message logging.info(f'Task {value} is done') except asyncio.CancelledError: # log the cancellation logging.debug(f'Task {value} cancelled') # re-raise the exception raise # main coroutine async def main(): # get the root logger log = logging.getLogger() # log all messages, debug and up log.setLevel(logging.DEBUG) # log a message logging.info(f'Main is starting') # schedule many tasks tasks = [asyncio.create_task(task(i)) for i in range(10)] # let them run a moment await asyncio.sleep(0.5) # cancel some tasks tasks[0].cancel() tasks[1].cancel() tasks[2].cancel() # let things finish up await asyncio.sleep(0.5) # log a message logging.info(f'Main is done') # start the event loop asyncio.run(main()) |
Running the program first starts the event loop and runs the main() coroutine.
The main() coroutine runs and sets the logging level, then logs an initial info message.
It then schedules all 10 tasks and stores the Task objects in a list.
The main() coroutine then sleeps a moment to allow the tasks to run.
The tasks run, first logging a message and then suspending with a sleep.
The main() coroutine resumes and then cancels the first 3 tasks. It then suspends again and allows the tasks to be completed.
The tasks resume. The first three tasks raise a CancelledError exception, which is caught, logged, and re-raised, terminating the tasks.
The remaining tasks log an info message and terminate normally.
The main() coroutine resumes and logs a final info message.
This highlights how we can log CancelledError exceptions within tasks without disrupting the normal propagation of task cancellation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
INFO:root:Main is starting INFO:root:Task 0 is starting INFO:root:Task 1 is starting INFO:root:Task 2 is starting INFO:root:Task 3 is starting INFO:root:Task 4 is starting INFO:root:Task 5 is starting INFO:root:Task 6 is starting INFO:root:Task 7 is starting INFO:root:Task 8 is starting INFO:root:Task 9 is starting DEBUG:root:Task 0 cancelled DEBUG:root:Task 1 cancelled DEBUG:root:Task 2 cancelled INFO:root:Task 3 is done INFO:root:Task 4 is done INFO:root:Task 5 is done INFO:root:Task 6 is done INFO:root:Task 7 is done INFO:root:Task 8 is done INFO:root:Task 9 is done INFO:root:Main is done |
Next, we will explore how we can automatically log exceptions that are never retrieved in asyncio.
Example of Logging Never-Retrieved Exceptions in Asyncio
We can explore an example of automatically logging never-retrieved exceptions in asyncio.
The asyncio event loop will log silent exceptions when it is shut down. These silent exceptions are those exceptions raised by a task, but never retrieved by another task.
You can learn more about silent or never-retrieved exceptions in the tutorial:
We can log these exceptions using the logging infrastructure.
This can be achieved by configuring a log handler function that takes the event loop object and a dict “context” as arguments.
This function can then retrieve the raised exception from the “context” dict via the “exception” key and log it.
We should not log exceptions using logging.exception() when outside of a try-except structure as it can cause problems. Instead, we should use logging.error() or similar.
We will use logging.error() in this case.
logging.ERROR: Due to a more serious problem, the software has not been able to perform some function.
— logging — Logging facility for Python
1 2 3 4 5 6 |
# define an exception handler def exception_handler(loop, context): # get the exception object exp = context['exception'] # log exception logging.error(f'Never-retrieved exception: {exp}') |
We can then configure the asyncio event loop to call our custom handler for each never-retrieved exception on shutdown.
This can be achieved by first retrieving the event loop object via the asyncio.get_running_loop() function, then calling the set_exception_handler() function to set the handler function.
1 2 3 4 5 |
... # get the event loop loop = asyncio.get_running_loop() # set the exception handler loop.set_exception_handler(exception_handler) |
Tying this together, the complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# SuperFastPython.com # example of automatically logging never-retrieved exceptions import logging import asyncio # define an exception handler def exception_handler(loop, context): # get the exception object exp = context['exception'] # log exception logging.error(f'Never-retrieved exception: {exp}') # task that does work and logs async def task(value): # log a message logging.info(f'Task {value} is starting') # simulate doing work await asyncio.sleep(1) # check if it should fail if value % 2 == 0: raise Exception(f'Task {value} failed') # log a message logging.info(f'Task {value} is done') # main coroutine async def main(): # get the root logger log = logging.getLogger() # log all messages, debug and up log.setLevel(logging.DEBUG) # get the event loop loop = asyncio.get_running_loop() # set the exception handler loop.set_exception_handler(exception_handler) # log a message logging.info(f'Main is starting') # schedule all tasks tasks = [asyncio.create_task(task(i)) for i in range(10)] # allow tasks to complete await asyncio.sleep(1.2) # log a message logging.info(f'Main is done') # start the event loop asyncio.run(main()) |
Running the program first starts the event loop and runs the main() coroutine.
The main() coroutine runs and sets the logging level, configures the event loop exception handler function, and then logs an initial info message.
It then schedules all 10 tasks and stores the Task objects in a list.
The main() coroutine then suspends for enough time for all tasks to be completed.
The tasks run, first logging a message and then suspending with a sleep.
The tasks resume and check their argument value. If it is even, an exception is raised, terminating the task, otherwise, an info message is logged.
The main() coroutine resumes and reports the final info message.
The event loop then calls our custom exception handler function for each exception that was never retrieved. We can see error log messages for all of the tasks that received an even argument.
This highlights how we can automatically log never-retrieved exceptions when the event loop is shut down.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
INFO:root:Main is starting INFO:root:Task 0 is starting INFO:root:Task 1 is starting INFO:root:Task 2 is starting INFO:root:Task 3 is starting INFO:root:Task 4 is starting INFO:root:Task 5 is starting INFO:root:Task 6 is starting INFO:root:Task 7 is starting INFO:root:Task 8 is starting INFO:root:Task 9 is starting INFO:root:Task 1 is done INFO:root:Task 3 is done INFO:root:Task 5 is done INFO:root:Task 7 is done INFO:root:Task 9 is done INFO:root:Main is done ERROR:root:Never-retrieved exception: Task 8 failed ERROR:root:Never-retrieved exception: Task 6 failed ERROR:root:Never-retrieved exception: Task 4 failed ERROR:root:Never-retrieved exception: Task 2 failed ERROR:root:Never-retrieved exception: Task 0 failed |
Further Reading
This section provides additional resources that you may find helpful.
Python Asyncio Books
- Python Asyncio Mastery, Jason Brownlee (my book!)
- Python Asyncio Jump-Start, Jason Brownlee.
- Python Asyncio Interview Questions, Jason Brownlee.
- Asyncio Module API Cheat Sheet
I also recommend the following books:
- Python Concurrency with asyncio, Matthew Fowler, 2022.
- Using Asyncio in Python, Caleb Hattingh, 2020.
- asyncio Recipes, Mohamed Mustapha Tahrioui, 2019.
Guides
APIs
- asyncio — Asynchronous I/O
- Asyncio Coroutines and Tasks
- Asyncio Streams
- Asyncio Subprocesses
- Asyncio Queues
- Asyncio Synchronization Primitives
References
Takeaways
You now know how to log exceptions from asyncio.
Did I make a mistake? See a typo?
I’m a simple humble human. Correct me, please!
Do you have any additional tips?
I’d love to hear about them!
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Louis Hansel on Unsplash
Do you have any questions?