Exception handling in asyncio
In most situations, exception handling in asyncio is as you'd expect in your typical Python application.
import asyncio
async def bad():
raise Exception()
def main():
try:
asyncio.run(bad())
except Exception:
print("Handled exception")
>>> main()
Handled exception
However, there are some situations where things get interesting.
asyncio.gather
You can use asyncio.gather to launch several coroutines, which are then executed concurrently.
import asyncio
async def hello():
# To simulate a network call, or other async work:
await asyncio.sleep(1)
print('hello')
async def main():
await asyncio.gather(
hello(),
hello(),
hello()
)
>>> asyncio.run(main())
hello
hello
hello
What happens if one of the coroutines raises an exception? The default behavior is for the first exception raised by any of the coroutines to be propagated to the call site of asyncio.gather. The other coroutines continue to run.
If more than one of the coroutines raises an exception, you won't be aware of it. If you need to run some clean up code to handle an exception (for example, rolling back a transaction), then you could potentially miss it if a different coroutine raises an exception first.
You might also wonder when the exception is handled - is it as soon as it's raised, or only when all of the coroutines have completed?
Fortunately, asyncio.gather has an option called return_exceptions, which returns the exceptions instead of raising them.
import asyncio
async def good():
return 'OK'
async def bad():
raise ValueError()
async def main():
responses = await asyncio.gather(
bad(),
good(),
bad(),
return_exceptions=True
)
print(responses)
# >>> [ValueError(), 'OK', ValueError()]
We are now aware of every exception which happened. But as a programmer, what do we do with a list of values and exceptions? It feels quite alien.
To solve this problem, I created a library called asyncio_tools, which wraps gather
to make it more user friendly.
import asyncio_tools
async def good():
return 'OK'
async def bad():
raise ValueError()
async def main():
response = await asyncio_tools.gather(
bad(),
good(),
bad(),
)
# We can easily get just the successful results
print(response.successes)
# >>> ['OK']
# And the exceptions.
print(response.exceptions)
# >>> [ValueError(), ValueError()]
# We can easily check if we got a certain type of exception
if ValueError in response.exception_types:
print('Received a ValueError exception')
# We can combine all of the exceptions into a 'CompoundException':
exception = response.compound_exception()
if exception:
raise exception()
If we raise a CompoundException
, it allows us to return information about several
exceptions. When we catch such an exception, we can do the following:
async def main():
try:
await some_coroutine()
except asyncio_tools.CompoundException as exception:
print(exception)
# >>> 'CompoundException, 2 errors [ValueError, ValueError]'
if ValueError in exception.exception_types:
print('Caught a ValueError')
This makes handling exceptions in concurrent code easier - I encourage you to check it out.
Posted on: 23 Feb 2020
Have any comments or feedback on this post? Chat with us on GitHub.