asyncio Task exception was never retrieved (tested)
asyncio prints Task exception was never retrieved when a task raised and nothing ever awaited it or read its exception, typically a fire-and-forget create_task(). The error is lost and the program carries on. Keep a reference and add a done callback, or use gather or TaskGroup.
1. Reproduce "Task exception was never retrieved"
import asyncio, gc
async def fails():
raise ValueError("boom")
async def main():
asyncio.create_task(fails()) # fire-and-forget: nobody awaits it
await asyncio.sleep(0.1)
print("main finished normally")
asyncio.run(main())
gc.collect()
print("still running after asyncio.run(): the error did not propagate")
Output:
Task exception was never retrieved
future: <Task finished name='Task-2' coro=<fails() done, defined at /tmp/te376/b1.py:2> exception=ValueError('boom')>
Traceback (most recent call last):
File "/tmp/te376/b1.py", line 3, in fails
raise ValueError("boom")
ValueError: boom
main finished normally
still running after asyncio.run(): the error did not propagate
The task raised, but nobody awaited it or asked for its result, so the exception has nowhere to go. asyncio logs it when the finished Task object is garbage-collected, and here that happened before main finished normally was printed: the only reference to the task was the discarded return value of create_task. The program carries on as if nothing failed. This is a lost error, not just a noisy warning.
2. Three ways to retrieve the exception
import asyncio
async def fails():
raise ValueError("boom")
background = set()
def report(t: asyncio.Task):
background.discard(t)
if not t.cancelled() and t.exception() is not None:
print("task failed:", repr(t.exception()))
async def main():
t = asyncio.create_task(fails())
background.add(t) # keep a strong reference
t.add_done_callback(report) # retrieve the exception
await asyncio.sleep(0.1)
# or: gather with return_exceptions=True
print(await asyncio.gather(fails(), asyncio.sleep(0, "ok"), return_exceptions=True))
# or: TaskGroup propagates the error to the caller
try:
async with asyncio.TaskGroup() as tg:
tg.create_task(fails())
except* ValueError as eg:
print("TaskGroup raised:", repr(eg.exceptions[0]))
asyncio.run(main())
Output:
task failed: ValueError('boom')
[ValueError('boom'), 'ok']
TaskGroup raised: ValueError('boom')
- Fire-and-forget tasks: keep a strong reference (a set) and add a done callback that calls
t.exception()and logs it. Callingexception()counts as retrieving it, so the warning goes away and you decide what to do with the error. Checkt.cancelled()first:exception()raisesCancelledErroron a cancelled task. gather(..., return_exceptions=True): exceptions come back as values in the result list.asyncio.TaskGroup(Python 3.11+): a failing child cancels the others and the error is raised from theasync withblock as anExceptionGroup, caught here withexcept*.
3. The sibling warning: "Task was destroyed but it is pending!"
import asyncio
async def slow():
await asyncio.sleep(10)
async def main():
asyncio.create_task(slow())
loop = asyncio.new_event_loop()
loop.run_until_complete(main())
loop.close() # closes with slow() still pending
Output:
Task was destroyed but it is pending!
task: <Task pending name='Task-2' coro=<slow() done, defined at /tmp/te376/b3.py:2> wait_for=<Future pending cb=[Task.task_wakeup()]>>
import asyncio
async def slow():
await asyncio.sleep(10)
async def main():
asyncio.create_task(slow())
asyncio.run(main()) # cancels leftover tasks before closing
print("asyncio.run: no warning")
Output:
asyncio.run: no warning
This one means the event loop was closed (or the task was garbage-collected) while the task was still running. With a manual new_event_loop() / loop.close() it appears; asyncio.run() cancels leftover tasks before closing the loop, so the same code under asyncio.run is silent, though the work in slow() was still cut short. Await or cancel your background tasks before shutdown, and keep a reference to them while they run.
Checklist
- Find the task in the message (
coro=<fails() ...>) and its traceback: that is the real error. - Never drop the return value of
create_task: keep it in a set and add a done callback that checksexception(). - Prefer
TaskGrouporgatherwhen the caller should see the failure. - Use
asyncio.run()rather than managing the loop by hand, and await or cancel background tasks before exit.
Related: asyncio RuntimeError: Event loop is closed, asyncio TimeoutError and CancelledError.
Frequently asked questions
What does Task exception was never retrieved mean?
A task raised an exception but was never awaited and its exception was never read; asyncio logs it when the task is garbage-collected. The error did not reach your code.
How do I handle exceptions in asyncio.create_task?
Keep the task in a set and add a done callback that calls task.exception() (after checking task.cancelled()), or await it via gather or a TaskGroup.
What does Task was destroyed but it is pending mean?
The loop was closed or the task garbage-collected while it was still running. asyncio.run() cancels leftover tasks first, so it does not print this.
Written with AI assistance. Measured on 2026-10-23 with Python 3.13.5 on Linux. Each block was run verbatim (python -u, stdout and stderr together) and the output pasted unedited. PlainHeaders is our extension; Python is free.