@3FANG

Как обрабатывать ошибки в асинхронном коде?

У меня возникает предупреждение Task exception was never retrieved. Я не понимаю почему, ибо я обрабатываю возникающие ошибки в блоке try/except. Начал гуглить и наткнулся на подобный вопрос. Здесь я понял, что если сохранить ссылку на исключение (т.е. в переменную), то оно возникнет не сразу, а когда цикл будет завершен. Но почему блок try/except не обрабатывает исключение?

Ни так:
import asyncio


async def exc():
    print(1 / 0)
    
loop = asyncio.get_event_loop()

try: 
    loop.create_task(exc())
except ZeroDivisionError as ex:
    print(f"Ошибка {ex} обработана")

try:
    loop.run_forever()
except KeyboardInterrupt:
    loop.stop()
    loop.close()


Ни так:
import asyncio


async def exc():
    print(1 / 0)
    
loop = asyncio.get_event_loop()

loop.create_task(exc())

try:
    try:
        loop.run_forever()
    except ZeroDivisionError as ex:
        print(f"Ошибка {ex} обработана")
except KeyboardInterrupt:
    loop.stop()
    loop.close()


Если сохранить ссылку на объект исключения, то ничего не меняется, лишь исключение выбрасывается не сразу, а после остановки цикла.

Что я упускаю?
  • Вопрос задан
  • 134 просмотра
Решения вопроса 1
@Everything_is_bad
1. обрабатывай не на уровне вызова create_task, а на уровне exc, либо уровнем выше create_task или там где ожидаешь ответ.
2. а зачем тебе run_forever? давно уже run используют
Начал гуглить и наткнулся на подобный вопрос.
на год вопроса и ответов тоже обращай внимание
Ответ написан
Пригласить эксперта
Ответы на вопрос 1
Vindicar
@Vindicar
RTFM!
try: 
    loop.create_task(exc())
except ZeroDivisionError as ex:
    print(f"Ошибка {ex} обработана")

1. Ты создаёшь задачу на базе корутины exc(). Созданная задача не выполнится немедленно, а только встанет в очередь исполнения (хотя в питоне 3.12 это поведение можно изменить, но по умолчанию это так). При этом с корутиной ассоциируется future-объект, который находится в состоянии "ожидание", так как корутина ещё не завершила работу.
2. Ты проверяешь, не возникло ли исключение в процессе создания задачи. Это может произойти, только если exc() - не корутина. В остальных случаях операция будет успешна независимо от содержания exc(), так как см. пункт 1.
3. Ты вызываешь loop.run_forever(). Рабочий цикл (loop) смотрит в очередь исполнения, и видит в нём только корутину exc(). Она получает управление.
4. Корутина exc() выбрасывает исключение, но не ловит его. Ассоциированный с корутиной future-объект переходит из состояния "ожидание" в состояние "отказ", и сохраняет информацию об исключении. exc() завершает работу.
5. Рабочий цикл проверяет, кто хранит ссылку на task - и понимает, что никто. Как следствие, даже в будущем никто не сможет узнать, что корутина выкинула исключение. Поскольку рабочий цикл - штука типовая, он понятия не имеет, что делала наша корутина и как надо реагировать на исключение в ней. А потому единственный вариант для него - написать в журнал работы о непойманном исключении и надеяться, что программист это увидит и поправит.
task = asyncio.create_task(exc())
try:
    # await asyncio.gather(task)  # <- gather() не нужно, если у тебя одна задача
    await task
except ZeroDivisionError as ex:
    print(f"Ошибка {ex} обработана")

1. Ты создаёшь задачу (успешно), а потом с помощью await-вызова просишь дождаться её завершения и получить результат.
2. Корутина main() приостанавливает своё выполнение и сохраняет свой контекст, а также встаёт в очередь, ожидая, когда сработает future-объект, связанный с task.
3. Рабочий цикл (loop) asyncio смотрит в очередь выполнения и видит, что корутина exc() готова выполняться (она не находится в await вызове, а только начала работу).
4. Корутина exc() получает управление, выполняется, и генерирует исключение, которое не поймано внутри этой корутины. Future-объект, связанный с этой корутиной, переходит из состояния "ожидание" в состояние "отказ", и сохраняет информацию об исключении. Корутина exc() завершает выполнение. При этом о её future-объекте знает корутина main(), поэтому рабочий цикл не дёргается по этому поводу - у main() будет возможность отреагировать на происходящее.
5. Рабочий цикл (loop) смотрит в очередь выполнения и видит, что там только корутина main(), причём она готова выполняться - future-объект, который она ждёт, более не находится в состоянии ожидания.
6. Корутина main() получает управление и восстанавливает свой контекст, продолжая с того места, где она остановилась. Так как future-объект находится в состоянии "отказ", оператор await читает из него информацию об исключении и перевыбрасывает это исключение внутри main().
7. Это исключение обрабатывается блоком try-except в main() как обычно.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы
SpectrumData Екатеринбург
от 150 000 до 200 000 ₽
Гринатом Москва
от 150 000 ₽
DIGITAL SECTOR Краснодар
от 150 000 до 250 000 ₽