8.6. Винятки

Винятки всередині скрипту asyncio поводяться майже так само, як у звичайному Python – вони поширюються вгору по ланцюжку викликів, поки щось їх не перехопить. Майже тому, що задачі виконуються паралельно, тому шлях «вгору» не є шляхом, який створив задачу. Ця сторінка описує, куди потрапляють винятки в кожній з поширених конфігурацій.

8.6.1. Всередині однієї корутини

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

Нічого специфічного для asyncio тут немає – блок except бачить винятки, згенеровані всередині fetch, так само, ніби це був звичайний виклик функції.

8.6.2. У задачі, яку програма очікує

Коли корутина, що виконується як Task, генерує виняток, цей виняток зберігається на задачі. Наступного разу, коли щось awaits цю задачу, виняток повторно генерується на await

task = asyncio.create_task(may_fail())
try:
    result = await task
except OSError:
    log("may_fail failed")

Те саме стосується asyncio.gather(). Стандартна поведінка – одна дочірня задача генерує виняток, інші скасовуються, виняток поширюється за межі gather – є наслідком цього механізму.

8.6.3. У задачі, яку ніхто не очікує

Задача, яку ніхто ніколи awaits – це той випадок, що потребує уваги. Виняток все одно виникає; цикл помічає, що задача завершилася з необробленим винятком; але немає await, на якому він міг би спливти. Стандартна поведінка – вивести трасування стеку через sys.stderr і продовжувати роботу – що прийнятно для неприсутньої діагностики, але погано підходить для програми, яка хотіла знати про це.

Правильне виправлення зазвичай полягає в тому, щоб await задачу. Або безпосередньо, запам’ятавши дескриптор і await його під час завершення, або неявно через gather() або wait_for(). Шаблон «завершення програми» зі сторінки Таймаути та скасування охоплює цей випадок для довгоживучих фонових задач, які породжує типовий скрипт.

8.6.4. Власний обробник винятків

Коли мовчазного виведення трасування і продовження роботи недостатньо, цикл надає хук – Loop.set_exception_handler – який програма може перевизначити для іншої дії:

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)

Аргумент context – це словник з ключами 'message', 'exception' і 'future'. Виняток може бути відсутнім у деяких попереджувальних подіях, тому в прикладі використовується .get().

Типові варіанти використання – записати збій на флеш-пам’ять, мигнути LED помилки або ескалувати до перезапуску через watchdog. Сторінка керування циклом охоплює всю поверхню хуків циклу.

8.6.5. KeyboardInterrupt

Коли скрипт зупиняється ззовні – зазвичай за запитом IDE зупинити його – запит надходить всередину скрипту як KeyboardInterrupt. Всередині asyncio.run() він поширюється так само, як і будь-який інший необроблений виняток: main скасовується, кожна задача, яку відстежує цикл, також скасовується, і KeyboardInterrupt повторно генерується за межами asyncio.run(). Блоки finally виконуються на виході, тому той самий шаблон очищення зі сторінки скасування обробляє цей випадок.