11.6. Exceptions¶
Exceptions inside an asyncio script behave almost the same as in regular Python – they propagate up the call chain until something catches them. Almost because tasks are running in parallel, so the path “up” is not the path that created the task. This page covers where exceptions go in each of the common shapes.
11.6.1. Inside one coroutine¶
A try/except inside a coroutine catches exceptions
raised by anything it awaits, in the usual way:
async def fetch_with_retry(url):
for attempt in range(3):
try:
return await fetch(url)
except OSError as e:
last_error = e
raise last_error
Nothing asyncio-specific here – the except clause sees
exceptions raised inside fetch as if it had been a
regular function call.
11.6.2. In a task the application is awaiting¶
When a coroutine running as a Task raises,
the exception is stored on the task. The next time something
awaits that task, the exception is re-raised at the
await:
task = asyncio.create_task(may_fail())
try:
result = await task
except OSError:
log("may_fail failed")
The same applies to asyncio.gather(). The default
behaviour – one child raises, others get cancelled, the
exception propagates out of the gather – comes from this
mechanism.
11.6.3. In a task nobody awaits¶
A task that nobody ever awaits is the case that needs
attention. The exception still happens; the loop notices the
task finished with an unhandled exception; but there is no
await for it to surface at. The default behaviour is to
print a traceback through sys.stderr and continue
running – which is fine for an unattended diagnostic, but a
poor fit for an application that wanted to know.
The right fix is usually to await the task. Either
directly, by remembering the handle and awaiting it during
shutdown, or implicitly through gather() or
wait_for(). The
Timeouts and cancellation page’s “shutting an
application down” pattern catches this case for the
long-lived background tasks a typical script spawns.
11.6.4. Custom exception handler¶
When silent traceback-and-continue is not enough, the loop
exposes a hook – Loop.set_exception_handler – the application can
override to do something else:
def handler(loop, context):
print("asyncio:", context.get("message"))
if "exception" in context:
sys.print_exception(context["exception"])
loop = asyncio.get_event_loop()
loop.set_exception_handler(handler)
The context argument is a dict with keys
'message', 'exception', and 'future'. The
exception may be missing on certain warning-style events,
which is why the example uses .get().
Typical uses are to log the failure to flash, blink an error LED, or escalate to a watchdog reboot. The loop control page covers the full surface of loop hooks.
11.6.5. KeyboardInterrupt¶
When a script is stopped from the outside – usually by the
IDE asking it to halt – the request arrives inside the
script as a KeyboardInterrupt. Inside
asyncio.run() it propagates the way any other
unhandled exception would: main is cancelled, every task
the loop is tracking gets cancelled too, and the
KeyboardInterrupt is re-raised out of
asyncio.run(). finally clauses run on the way out,
so the same cleanup pattern from the cancellation page is
what handles it.