Task exception was never retrieved: что это и как исправить

Предупреждение Task exception was never retrieved в консоли Python означает, что задача asyncio завершилась с исключением, но никто не прочитал результат этой задачи — исключение «зависло» внутри объекта Task и было обнаружено только сборщиком мусора. Это не падение программы, а сигнал о логической ошибке: где-то в асинхронном коде потерялся await или обработчик ошибок.

Проблема особенно коварна тем, что само исключение не прерывает выполнение цикла событий. Программа продолжает работать, а разработчик узнаёт о сбое постфактум — по сообщению в логе, которое может появиться через секунды или минуты после реальной ошибки. В этой статье разберём механику предупреждения, типичные сценарии его возникновения и способы корректной обработки исключений в задачах asyncio.

Что именно означает это предупреждение

Когда вы создаёте задачу через asyncio.create_task(), цикл событий запускает корутину в фоне. Если внутри корутины возникает исключение, оно не выбрасывается наружу сразу — вместо этого оно сохраняется внутри объекта задачи. Прочитать его можно через await task, task.result() или task.exception().

Если ни один из этих способов не использован, объект задачи рано или поздно удаляется сборщиком мусора. В этот момент asyncio замечает «непрочитанное» исключение и выводит предупреждение через exception handler цикла событий. Именно поэтому сообщение появляется с задержкой и часто указывает на строку, где задача была создана, а не где произошла ошибка.

Важно понимать: само предупреждение — не причина сбоя, а симптом того, что исключение в фоновой задаче осталось необработанным. Реальная проблема находится внутри корутины, и текст оригинального traceback обычно выводится рядом с предупреждением.

Типичные сценарии возникновения

Чаще всего предупреждение появляется в нескольких повторяющихся ситуациях. Знание этих паттернов помогает быстро локализовать проблему в своём коде.

  • 🔥 Fire-and-forget без обработки — задача создана через create_task(), но ссылка на неё нигде не сохраняется и результат не ожидается.
  • 🧩 Потерянный await — задача создана, но await task забыли или он находится в ветке кода, которая не выполняется.
  • 📦 Сборка задач в список без gather — задачи создаются в цикле, но asyncio.gather() не вызывается, либо его результат игнорируется.
  • ⏱️ Отмена и таймауты — исключение возникает после asyncio.wait_for() или отмены задачи, а код не проверяет её финальное состояние.

Отдельный случай — преждевременная сборка мусора. Если ссылка на задачу не сохранена, объект Task может быть уничтожен ещё до завершения корутины. В документации asyncio прямо рекомендуется хранить ссылки на фоновые задачи, иначе возможно не только предупреждение, но и неожиданная отмена задачи посреди работы.

📊 Где вы чаще всего встречаете Task exception was never retrieved?
В фоновых задачах create_task
При работе с asyncio.gather
В телеграм-ботах и веб-фреймворках
В чужом коде или библиотеках

Как найти источник ошибки

Первое действие — внимательно прочитать вывод полностью. Перед строкой Task exception was never retrieved обычно печатается оригинальный traceback исключения, которое произошло внутри корутины. Именно он указывает на реальную причину: сетевой сбой, KeyError, ошибку типов и так далее.

Если traceback не даёт ответа, включите режим отладки asyncio. В нём цикл событий логирует дополнительную информацию, включая место создания задачи:

import asyncio

asyncio.run(main(), debug=True)

Альтернатива — установить переменную окружения PYTHONASYNCIODEBUG=1 перед запуском скрипта. В отладочном режиме сообщения о необработанных исключениях содержат больше контекста, что упрощает поиск «потерянной» задачи в большом проекте.

Способы исправления

Универсального «лекарства» не существует — правильное решение зависит от того, зачем задача создавалась. Ниже основные рабочие подходы.

Способ 1: дождаться результата. Если результат задачи нужен, просто используйте await:

async def worker():

raise ValueError("что-то пошло не так")

async def main():

task = asyncio.create_task(worker())

try:

await task

except ValueError as e:

print(f"Обработано: {e}")

Способ 2: обернуть корутину в try/except. Для фоновых задач, результат которых не нужен, надёжнее перехватывать исключения внутри самой корутины и логировать их:

async def safe_worker():

try:

await risky_operation()

except Exception:

logger.exception("Фоновая задача завершилась с ошибкой")

Способ 3: колбэк завершения. Если задача должна работать независимо, добавьте обработчик через add_done_callback(), который прочитает исключение:

def on_done(task):

if not task.cancelled() and task.exception():

logger.error("Ошибка задачи", exc_info=task.exception())

task = asyncio.create_task(worker())

task.add_done_callback(on_done)

☑️ Проверка кода на необработанные исключения задач

Выполнено: 0 / 5

Сравнение подходов к обработке

Выбор метода зависит от роли задачи в программе. Таблица ниже помогает сориентироваться.

ПодходКогда применятьОсобенности
await taskРезультат нужен вызывающему кодуИсключение поднимается в точке await
try/except внутри корутиныФоновые задачи без возвратаОшибка логируется, цикл не затрагивается
add_done_callbackНезависимые задачи с пост-обработкойНужно явно вызвать task.exception()
gather(return_exceptions=True)Пакетный запуск нескольких задачИсключения возвращаются как элементы списка
TaskGroup (Python 3.11+)Структурированный параллелизмОшибки объединяются в ExceptionGroup
⚠️ Внимание: подавлять предупреждение через глобальный exception handler или игнорировать его — плохая идея. Так вы скроете симптом, но реальные исключения в задачах продолжат теряться, что приведёт к трудноотлаживаемым сбоям в продакшене.

Особенности в библиотеках и фреймворках

На практике предупреждение часто приходит не из вашего кода напрямую, а из библиотек: aiogram, aiohttp, discord.py, FastAPI с фоновыми задачами. В этом случае исключение возникло внутри хендлера или middleware, а библиотека не стала его проксировать наружу.

Порядок действий здесь такой: сначала смотрите оригинальный traceback — он покажет, в каком хендлере произошла ошибка. Затем проверьте, предусмотрен ли библиотекой механизм обработки ошибок (например, error handlers в aiogram или @client.event с обработкой в discord.py). Конкретные имена механизмов различаются между версиями, поэтому сверяйтесь с документацией используемой версии.

Почему сообщение появляется через случайное время

Предупреждение генерируется деструктором объекта Task при сборке мусора. Момент сборки зависит от работы GC и настроек цикла, поэтому между реальной ошибкой и выводом сообщения может пройти заметное время. Включение debug-режима asyncio помогает связать предупреждение с местом создания задачи.

⚠️ Внимание: если предупреждение возникает только под нагрузкой или изредка, вероятная причина — гонка между отменой задачи и её завершением. Проверьте места, где используются wait_for, shield и явные вызовы cancel().

Профилактика: как писать код без этого предупреждения

Лучший способ борьбы с Task exception was never retrieved — дисциплина при работе с задачами. Сформулируем её в виде простых правил.

  • ✅ Всегда сохраняйте ссылку на созданную задачу, даже если результат не нужен — это защищает и от сборки мусора.
  • ✅ Для каждой задачи решите заранее: кто прочитает её результат или исключение.
  • ✅ Используйте TaskGroup в новых проектах на Python 3.11+ — он не позволит исключениям «утечь» незамеченными.
  • ✅ Логируйте исключения в фоновых корутинах с полным traceback через logger.exception().

Дополнительно полезно настроить собственный обработчик исключений цикла через loop.set_exception_handler() на этапе разработки — так необработанные ошибки задач будут попадать в вашу систему логирования с нужным контекстом, а не только в stderr.

Частые вопросы

Это предупреждение опасно? Программа ведь продолжает работать.

Само по себе предупреждение не останавливает программу, но оно означает, что какая-то операция завершилась ошибкой и никто об этом не узнал. В продакшене это может приводить к потере данных, неотправленным сообщениям или незакрытым ресурсам. Игнорировать его не стоит.

Почему traceback указывает на место создания задачи, а не на ошибку?

Сообщение выводится в момент уничтожения объекта задачи, и контекст создания — это то, что известно в этот момент. Однако выше предупреждения обычно печатается полный traceback исходного исключения — смотрите именно его. Режим отладки asyncio добавляет деталей.

Можно ли просто отключить это предупреждение?

Технически можно переопределить exception handler цикла событий, но это лишь скроет симптом. Исключения в задачах продолжат теряться, и вы лишитесь единственного сигнала о реальных сбоях. Правильный путь — обработать исключения в коде.

Предупреждение приходит из чужой библиотеки — что делать?

Сначала проверьте оригинальный traceback: часто ошибка на самом деле в вашем хендлере, который библиотека вызывает в своей задаче. Если проблема внутри самой библиотеки — обновите её до актуальной версии и проверьте, предусмотрен ли механизм обработки ошибок в используемой версии.

Помогает ли asyncio.gather решить проблему полностью?

Только если результат gather реально обрабатывается. По умолчанию gather при первом же исключении поднимает его наружу, но остальные задачи продолжают работать — их исключения тоже могут остаться непрочитанными. С параметром return_exceptions=True все исключения возвращаются в списке результатов, и их нужно проверить вручную.