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')

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

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.