Читаем async-ошибки: словарь типовых сообщений
Сообщения asyncio пугают, пока их не переведёшь. Четыре типовые ошибки — причина, лечение, живой пример на каждую.
Редакция Питоники
Асинхронные traceback'ы многословны, а сообщения asyncio звучат как приговор: «never awaited», «never retrieved», «no running event loop». На деле это не экзотика, а четыре-пять типовых ситуаций, которые встречаются у каждого — просто у каждой есть точная причина и короткое лечение. Этот урок — словарь: сообщение, причина, лечение и запускаемый пример, где ошибку видно вживую. Словарь экономит часы: вместо гугления пугающей строки — знакомый пункт с точным лечением. Пробегись по таблице при первом чтении и вернись к ней, когда сообщение появится на твоём экране, — так словарь работает быстрее всего.
| Сообщение | Причина | Лечение |
|---|---|---|
| coroutine ... was never awaited | Корутину создали вызовом, но не запустили | await перед вызовом или asyncio.create_task |
| Task exception was never retrieved | Задача упала, результат никто не забрал | await task в try/except или task.exception() |
| RuntimeError: no running event loop | async-вызов вне asyncio.run | Вызывать из корутин, а не с верхнего уровня |
| TimeoutError от wait_for | Операция не успела в дедлайн | Обработать try/except или пересмотреть лимит |
| got Future attached to a different loop | Примитив создан вне цикла или циклов два | Создавать и использовать всё внутри одного asyncio.run |
Never awaited: корутина, которую никто не запустил
Ошибка номер один, ей посвящён целый ранний урок, но повторить стоит: вызов async-функции не выполняет тело — он создаёт объект-корутину. Запускают его await или create_task. Без них тело молчит, а Python оставляет предупреждение в консоли. Разберём на паре «создал — запустил»:
import asyncio
async def ping():
await asyncio.sleep(1)
return "pong"
async def main():
coro = ping() # создали объект, тело НЕ работает
print("это ещё не результат:", type(coro).__name__)
result = await coro # await запускает тело
print("а вот теперь результат:", result)
asyncio.run(main())
это ещё не результат: coroutine а вот теперь результат: pong
Строка coro = ping() отработала мгновенно и без ошибок — в этом коварство. Тело стартует только на await. А что будет, если await так и не случится? Запусти следующий пример и загляни в консоль целиком: на экране основного вывода будет одна строка, но над или под ней Python напечатает RuntimeWarning: coroutine 'ping' was never awaited. Предупреждение появляется не в момент ошибки, а при уборке мусора — когда никому не нужный объект-корутина уничтожается.
import asyncio
async def ping():
await asyncio.sleep(1)
return "pong"
async def main():
ping() # забыли await - тело не стартует
print("main закончился")
asyncio.run(main())
main закончился
Для охоты на такие предупреждения у Python есть режим строгости: запуск с флагом -W error превращает предупреждения в ошибки, а -X dev включает подробную диагностику разработки. В тестах и CI это ловит забытый await на этапе прогона — не в продакшене, где warning тонет в простыне логов.
Task exception was never retrieved: упала, но никто не смотрел
У исключений в фоновых задачах свои правила: create_task запускает корутину, и если та падает — программа не взрывается. Исключение лежит внутри объекта задачи и ждёт, когда за ним придут с await. Не дождались до конца программы — Python напомнит сообщением «Task exception was never retrieved»: честный сигнал, что в фоне что-то сломалось, а ты не в курсе.
import asyncio
async def bad():
await asyncio.sleep(1)
raise ValueError("упала в фоне")
async def main():
task = asyncio.create_task(bad())
await asyncio.sleep(2) # задача успела упасть, но никто не смотрел
print("main закончился, задача упала незамеченной")
asyncio.run(main())
main закончился, задача упала незамеченной
Исключение в фоновой задаче не взрывается сразу: оно лежит внутри объекта задачи, пока кто-нибудь не придёт за результатом. Лечение — забрать: оберни await задачи в try/except, и падение станет обычной ошибкой, которую ты обрабатываешь там же, где запускал.
import asyncio
async def bad():
await asyncio.sleep(1)
raise ValueError("упала в фоне")
async def main():
task = asyncio.create_task(bad())
try:
await task # забираем результат или исключение
except ValueError as e:
print("поймали ошибку задачи:", e)
asyncio.run(main())
поймали ошибку задачи: упала в фоне
Второй способ — метод task.exception(): вызвать его можно, когда задача завершилась, и он вернёт исключение на руки, не поднимая его в твоём коде. Удобно для проверок после того, как основная работа кончилась.
Чтобы такие сообщения не всплывали неожиданно, у цикла событий есть свой обработчик: loop.set_exception_handler получает все необработанные случаи — включая never retrieved — и пишет их куда скажешь: в алерты, в файл, в систему мониторинга. По умолчанию всё уходит в логи встроенного логгера asyncio, которые редко кто читает, — вот и собираются сюрпризы к концу смены.
import asyncio
async def bad():
await asyncio.sleep(1)
raise ValueError("упала в фоне")
async def main():
task = asyncio.create_task(bad())
await asyncio.sleep(2)
print("задача завершена:", task.done())
exc = task.exception() # забрали исключение - предупреждения не будет
print("внутри задачи:", exc)
asyncio.run(main())
задача завершена: True внутри задачи: упала в фоне
RuntimeError: no running event loop
Часть asyncio-инструментов требует работающего цикла событий: create_task, Queue, Semaphore и их друзья планируют работу через цикл, а он живёт только внутри asyncio.run. Вызов с верхнего уровня файла — до запуска цикла или после его закрытия — даёт RuntimeError: no running event loop. Лечение одно: такие вызовы делаются из корутин, то есть внутри main.
import asyncio
async def inner():
return "данные"
try:
asyncio.create_task(inner()) # вне asyncio.run цикла нет
except RuntimeError as e:
print("RuntimeError:", e)
async def main():
print("внутри asyncio.run задачи работают")
asyncio.run(main())
RuntimeError: no running event loop внутри asyncio.run задачи работают
Из того же семейства — сообщение про «Future attached to a different loop»: примитив создали на одном цикле, а используют на другом. Причина почти всегда одна: два вызова asyncio.run в программе или примитив, созданный на верхнем уровне ещё до запуска цикла. Правило гигиены: один asyncio.run на программу, всё асинхронное создаётся и живёт внутри него.
Отдельная ловушка — ноутбуки и продвинутые REPL: там цикл событий уже крутится, и asyncio.run ответит «cannot be called from a running event loop». Формулировка другая, причина та же — конфликт циклов: в блокноте корутины дожидаются await прямо в ячейке, без asyncio.run, и create_task работает без обёрток.
TimeoutError: не баг, а сигнал
Таймауты из урока 9 при срабатывании бросают TimeoutError — и новичок часто принимает его за поломку. На деле это самый честный сигнал в списке: операция просто не уложилась в дедлайн, wait_for её отменил и сообщил об этом. Что с ним делать — решать тебе: увеличить лимит, повторить запрос или считать операцию неудачной и идти дальше.
TimeoutError бывает и не от wait_for: его бросают низкоуровневые операции сокетов и сторонние библиотеки со своими лимитами. Прежде чем лечить, посмотри traceback — кто источник: от этого зависит лечение. У wait_for это управление дедлайном, у библиотеки — её настройки повторов и лимитов, у сети — стабильность канала.
import asyncio
async def slow():
await asyncio.sleep(5) # слишком долго для дедлайна
return "успел"
async def main():
try:
await asyncio.wait_for(slow(), timeout=2)
except TimeoutError:
print("TimeoutError: не дождались за 2 с")
print("программа продолжает работу")
asyncio.run(main())
TimeoutError: не дождались за 2 с программа продолжает работу
Обрати внимание: после перехвата программа жива — таймаут перехвачен, main дошёл до последней строки. Именно так строят устойчивые запросники: не «упало — упало», а «не успело — попробуем следующего поставщика».
Пачка ошибок: gather и return_exceptions
Когда операций много, забирать ошибки по одной утомительно. gather из урока 7 с параметром return_exceptions=True складывает и результаты, и исключения в один список — тип каждого элемента расскажет, чем кончилась операция. Так собирают отчёты по пачке запросов: посчитать успешные, разложить ошибки по типам.
Замечай заодно два запаха кода: голый gather без return_exceptions в пачке запросов почти всегда баг в ожидании — одна ошибка роняет отчёт; а фоновая задача без ожидания — потенциальный never retrieved. Оба случая не стреляют сразу, и именно поэтому их стоит проверять код-ревью и тестами: предупреждение дешевле аварии на проде.
import asyncio
async def may_fail(name, delay, fail):
await asyncio.sleep(delay)
if fail:
raise RuntimeError(name + ": не получилось")
return name + ": успех"
async def main():
results = await asyncio.gather(
may_fail("a", 1, False),
may_fail("b", 2, True),
may_fail("c", 3, False),
return_exceptions=True, # ошибки придут в списке результатов
)
for r in results:
print(type(r).__name__, "-", r)
asyncio.run(main())
str - a: успех RuntimeError - b: не получилось str - c: успех
Как читать сам traceback
И последнее умение из словаря — читать длинный traceback конкурентного кода. Правило то же, что в синхронном Python: начинай с последней строки — там класс и сообщение исключения; выше по списку «File ... line ...» — цепочка вызовов, приведшая к нему. В async-программе в середине может вклиниться служебная строка про задачу или таймаут — она показывает, через какой механизм исключение попало наружу, и тоже полезна.
trace = """
Traceback (most recent call last):
File "bot.py", line 12, in handle
data = await api.load(url)
File "api.py", line 40, in load
return await session.get(url)
TimeoutError: не дождались ответа
"""
lines = trace.strip().splitlines()
print("строк в traceback:", len(lines))
print("причина - последняя строка:", lines[-1])
print("место падения - предпоследняя:", lines[-2].strip())
строк в traceback: 6 причина - последняя строка: TimeoutError: не дождались ответа место падения - предпоследняя: return await session.get(url)
Чек-лист диагностики
- прочитай последнюю строку traceback — там класс и сообщение ошибки;
- ищи RuntimeWarning про never awaited рядом с вызовом корутины;
- у каждой фоновой задачи — await в try/except или проверка exception();
- один asyncio.run на программу, примитивы создаются внутри него;
- TimeoutError — сигнал про дедлайн, а не про поломку программы.
Диагностика async-программы быстро сводится к чек-листу: прочитай последнюю строку traceback; поищи never awaited — забытый await; вспомни, не дождался ли ты фоновой задачи; проверь, один ли в программе asyncio.run; если TimeoutError — решай, лимит или операция. Этот чек-лист закрывает подавляющее большинство стартер-ошибок, а умение тестировать такие ситуации приходит из раздела про pytest: там же научишься проверять, что код падает именно с нужным исключением, а не с любым. Дальше — финальный проект курса, где все эти инструменты соберутся в один диспетчер задач.
Каждое пугающее сообщение asyncio — это точная диагностика: программа не может запустить корутину, не нашла цикл событий или не дождалась операции. Причина у сообщения одна, и лечение тоже.
Сначала предскажи ответ в голове — это главный навык программиста.
import asyncio
async def f():
return 1
async def main():
c = f()
print(type(c).__name__)
await c
asyncio.run(main())
import asyncio
async def fail():
raise ValueError("x")
async def main():
r = await asyncio.gather(fail(), return_exceptions=True)
print(type(r[0]).__name__)
asyncio.run(main())
import asyncio
async def job():
await asyncio.sleep(3)
async def main():
try:
await asyncio.wait_for(job(), timeout=1)
print("успел")
except TimeoutError:
print("таймаут")
asyncio.run(main())
1. Что означает RuntimeWarning: coroutine ... was never awaited?
2. Почему сообщение «Task exception was never retrieved» часто появляется в конце логов, а не сразу?
3. asyncio.create_task() на верхнем уровне файла даёт «RuntimeError: no running event loop». Как лечить?
4. wait_for бросил TimeoutError. Что это значит?
5. Что даёт gather(..., return_exceptions=True) при падении одной из корутин?
6. С какой строки стоит начинать чтение длинного traceback?
Напиши обёртку safe(coro): она дожидается переданную корутину и возвращает её результат, а если та упала — строку «ошибка: <текст>» вместо исключения. Проверь на двух корутинах: bad() поднимает ValueError с текстом «boom», good() через секунду возвращает 42. Напечатай результат safe для каждой: сначала для bad, потом «ok:» и результат good.
Почему предупреждение never awaited видно не сразу?
Оно печатается при уничтожении объекта-корутины сборщиком мусора, а не в момент вызова: вызов async-функции легален, просто бесполезен. Поэтому «неработающая» функция может молчать до конца программы. Привычка: после вызова async-функции сразу смотри, стоит ли await или create_task.
Как найти, какая именно фоновая задача упала?
Сообщение «Task exception was never retrieved» печатается вместе с traceback задачи — там видны файл и строка. Чтобы ловить падения предсказуемо, дожидайся задачи вручную: await task в try/except в месте, где она нужна, или собирай пачки через gather с return_exceptions=True.
Можно ли вызывать asyncio.run дважды в одной программе?
Технически можно, но не нужно: каждый вызов создаёт и закрывает новый цикл, а примитивы из первого цикла (очереди, задачи, семафоры) со вторым несовместимы — отсюда ошибки про «different loop». Один asyncio.run на программу, вся асинхронная работа — внутри его корутин.
Как тестировать, что код правильно обрабатывает асинхронные ошибки?
В pytest есть плагин pytest-asyncio: тесты объявляются async def и внутри них можно дожидаться корутин и проверять исключения через pytest.raises. Основы самого pytest — фикстуры, raises, параметризация — разбираются в нашем отдельном разделе: начни с первого урока, схемы те же, что и для синхронного кода.
Понравился урок? Сошлитесь на него
«Исключение в фоновой задаче не взрывается сразу: оно лежит внутри объекта задачи, пока кто-нибудь не придёт за результатом.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
asyncio · Урок 4
Coroutine never awaited: главная ошибка новичка
Программа завершилась успешно и ничего не сделала: классика потерянного await. Разбираем корутин-призраков, честно смотрим на RuntimeWarning и выучиваем три лекарства.
asyncio · Урок 9
Таймауты: asyncio.wait_for и timeout
Сеть не отвечает — а программа не должна висеть вечно: wait_for ставит дедлайн на любую корутину, TimeoutError ловится try/except, а вместо падения отдаётся запасной ответ.
asyncio · Урок 11
Исключения в конкурентном коде
Одна корутина с ValueError способна уронить весь gather. Флаг return_exceptions=True превращает исключения в значения списка — пачка выживает, а отчёт собирается целиком.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
json · Урок 2
json.loads: чтение JSON из строки
Обратная дорога: текст JSON становится словарём Python одной командой json.loads — и по ключам можно ходить, считать и менять значения.
превратить json в словарь pythonиз строки в словарь python
pytest · Урок 3
Отчёт pytest: подробный вывод -v и разбор падения
Зелёная точка — скучный отчёт, и это хорошо. Настоящая сила pytest раскрывается при падении: он показывает строку, ожидание, реальность и разницу между ними.
pytest разбор падения тестаpytest читать отчёт
asyncio · Урок 3
asyncio.run: событийный цикл под капотом
Одна строка asyncio.run(main()) превращает обычный скрипт в асинхронную программу. Разбираем три работы этой строки: создать цикл, раскрутить корутину, убрать за собой.
asyncio.run python запускasyncio.run