Fix Task Exception Was Never Retrieved in Python
The Task exception was never retrieved part of a criticalterminalalert message is fixed by making sure every created async task is awaited, supervised, or has its result explicitly collected, and by fixing the earlier exception that caused the task to fail. The terminal line is a warning from Python’s asyncio event loop, not the root cause. Start with the traceback immediately before the warning, identify the failed operation and its lifecycle event, then give that task a clear owner.
Use this table to match the terminal output to the repair that fits it.
| Terminal Clue | Likely Underlying Problem | First Fix |
|---|---|---|
TimeoutError or a cancellation chained into a timeout |
A device, API call, lock, or service operation did not finish before its deadline | Investigate the operation that timed out; await the task and handle cancellation cleanly. |
Target page, context or browser has been closed |
A Playwright route, request, or callback continued after a page or browser began closing | Stop new work during shutdown and catch expected close-related errors inside the callback. |
WebSocketClosedError or StreamClosedError |
Code tried to write to a WebSocket after the client or browser tab disconnected | Stop sending after disconnect, and await or collect shutdown tasks. |
| A traceback naming an integration or library | A dependency-owned background task failed | Identify the named component, reproduce its triggering action, and check for an update or known issue. |
Only Task exception was never retrieved with little context |
The task was created without being awaited or observed | Add task ownership, structured concurrency, or a done callback that calls task.result(). |
What the Asyncio Error Means
An asyncio.Task runs a coroutine in the background. If that coroutine raises an exception and no code ever awaits the task, calls task.result(), or otherwise collects the failure, Python reports:
Task exception was never retrieved
“Never retrieved” refers to the exception result, not necessarily the task itself. The coroutine already ran and failed; the program simply left its failure unobserved.
The warning often appears late, such as when the event loop is shutting down or Python is cleaning up the task object. That timing can make the final warning look unrelated to the real failure. The useful information is usually earlier in the output:
future: <Task finished ... coro=<send_update() ...> exception=TimeoutError()>
Traceback (most recent call last):
...
TimeoutError
In that example, the actionable problem is the TimeoutError in send_update(). The missing result retrieval is a second bug that makes diagnosis harder.
The same warning can result from a device cancellation, a browser-close race, or a closed WebSocket, so the exception and component named before it determine the correct fix.
A criticalterminalalert prefix may come from your editor, terminal integration, test runner, or application log formatter. It does not change how Python tasks work.
How to Fix Task Exception Was Never Retrieved
Await tasks that must finish, and keep and retrieve results from intentional background tasks.
Step 1: Find the Original Exception and the Task Owner
- Copy the complete traceback, including lines above
Task exception was never retrieved. - Find the first meaningful exception near the bottom, such as
TimeoutError,ConnectionError,WebSocketClosedError, or a library-specific error. - Find the coroutine name shown after
coro=<...>and the first application or integration file in the traceback. - Identify what was happening at that moment: an API call, browser page close, WebSocket write, device command, scheduled refresh, or application shutdown.
A broad try/except around unrelated code may hide output while leaving the failed background task and underlying race intact.
I reproduce one problem at a time in a clean container or virtual environment because it removes machine-specific noise from the traceback.
If you need more task-creation context while debugging a local program, enable asyncio debug mode at the program entry point:
asyncio.run(main(), debug=True)
This can add information about where a task was created, which is especially useful when the traceback only points into a framework.
Step 2: Await Tasks That Must Finish
If the caller needs the operation’s result, create the task only when you will await it. An awaited task’s exception is automatically retrieved, so Python does not produce the warning.
import asyncio
import logging
logger = logging.getLogger(__name__)
async def refresh_state():
# Perform an API request, database action, or other async operation.
...
async def main():
task = asyncio.create_task(refresh_state())
try:
await task
except asyncio.CancelledError:
raise
except Exception:
logger.exception("State refresh failed")
# Return a fallback value, retry through controlled logic,
# or re-raise if this operation must stop the application.
The important part is await task. It both waits for completion and receives the task’s exception.
asyncio.CancelledError represents a request to stop the task, usually during shutdown. Perform required cleanup in a finally block, then re-raise it.
async def worker():
try:
await refresh_state()
finally:
# Close files, release a connection, or update local state.
pass
Use asyncio.TaskGroup when several tasks belong to one operation and should succeed or fail together. A task group gives those tasks a shared owner and waits for them before the surrounding block exits.
async with asyncio.TaskGroup() as group:
group.create_task(refresh_state())
group.create_task(update_cache())
If one task fails, the task group reports that failure to the parent operation instead of leaving it unobserved in the background.
Step 3: Supervise Intentional Background Tasks
Some work truly belongs in the background: periodic refreshes, notification delivery, event consumers, or long-lived listeners. In that case, do not discard the Task returned by asyncio.create_task().
Keep each task in a collection and attach a callback that retrieves and logs the result.
import asyncio
import logging
logger = logging.getLogger(__name__)
background_tasks = set()
def report_background_failure(task):
try:
task.result()
except asyncio.CancelledError:
# Expected when the application intentionally stops the task.
return
except Exception:
logger.exception("Background task failed")
def start_background(coroutine):
task = asyncio.create_task(coroutine)
background_tasks.add(task)
task.add_done_callback(background_tasks.discard)
task.add_done_callback(report_background_failure)
return task
Use it where you would otherwise fire and forget:
start_background(send_notification())
Calling task.result() inside report_background_failure() retrieves the exception. The callback does not repair the failed job by itself, but it changes an anonymous terminal warning into a useful, labeled application error.
Avoid this pattern:
asyncio.create_task(send_notification())
It has no owner, no shutdown policy, and no reliable place to handle failure.
Step 4: Cancel and Drain Background Work During Shutdown
Before closing clients, pages, sockets, or the event loop, stop the tasks that use them and wait for those tasks to settle.
async def stop_background_tasks():
tasks = list(background_tasks)
for task in tasks:
task.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
Call this as part of your application’s shutdown sequence before closing resources the tasks depend on. asyncio.gather(..., return_exceptions=True) waits for every task and retrieves any final exception rather than abandoning it.
A reliable shutdown order is:
- Mark the application, page, or connection as closing.
- Stop accepting new background work.
- Cancel work that uses the resource.
- Await the cancelled tasks.
- Close the browser, WebSocket, client session, device connection, or event loop.
This order prevents a task from trying to send data through something that another part of the application already closed.
Fixes for Page, WebSocket, and Device Lifecycle Races
Playwright Page-Closure Races
A common Playwright failure happens when an event handler begins a route continuation or request action just as the page, context, or browser closes. The operation may fail with an error that the target page, context, or browser is closed, while the task created for that operation goes unobserved. A Playwright page-close race can produce exactly this pattern.
Set a closing flag before calling page.close(), avoid starting new route work once closing begins, and catch only the expected close-related error inside the event callback.
Conceptually:
Handle the Playwright error that can occur when a route continues as a page closes.
The closing check reduces new work, but it cannot eliminate every race on its own. The targeted exception handler covers the interval where the page closes after the check but before route.continue_() completes.
When testing route handling and page-closure timing, a Playwright configuration can use:
page.set_default_timeout(500)
That sets the page’s default timeout to 500 milliseconds. It is useful for reproducing fast timeout and shutdown behavior, not as a universal production setting; confirm the actual timeout from the code that configures your page or shared Playwright fixture.
WebSocket Shutdown Errors
A WebSocket can close because a browser tab closes, a user navigates away, a network connection ends, or the server begins shutdown. A task that tries to write after that point can raise WebSocketClosedError or StreamClosedError.
Fix the lifecycle ordering:
- Mark the connection as closed when its disconnect handler runs.
- Prevent new writes once it is closed.
- Keep references to writer and cleanup tasks.
- Await or collect their results before disposing of the server-side connection.
Do not broadly suppress every WebSocket error. A closed socket during intentional disconnect is expected; a closed socket during an active session can indicate a real connectivity, proxy, or application-state issue.
Device and Integration Timeouts
Home automation, media-control, and network-device integrations can produce this warning when a command is cancelled or times out while waiting for a device operation. For example, an Apple TV pause operation can time out after a cancellation while acquiring an async resource, leaving the integration’s task failure visible in the terminal; this appears in a Home Assistant Apple TV traceback.
For an integration-owned error:
- Identify the integration and device operation named in the traceback.
- Reproduce the action once, such as pause, refresh, connect, or discovery.
- Check whether the device is reachable and whether the failed action is still occurring.
- Update the application and integration if the issue has already been corrected upstream.
- Report the complete traceback if the current version still fails.
Do not edit library files simply to hide the warning. The useful repair belongs in the component that owns the task and understands whether cancellation, a timeout, or a lost device connection is expected.
How to Confirm the Fix and Prevent a Repeat
After changing task ownership or shutdown behavior, test both the success path and the failure path.
- Trigger the operation that originally caused the warning.
- Intentionally close the page, browser, WebSocket client, or application while work is active.
- Confirm that expected cancellation does not create an unobserved task exception.
- Confirm that a real failure is logged with the operation name and full traceback.
- Confirm that no task starts after its required client, page, socket, or device has begun closing.
- Confirm that application shutdown waits for tracked background tasks before the event loop exits.
For prevention, adopt one simple rule: every asyncio.create_task() must have an owner. That owner should be a parent coroutine, a task group, a background-task registry, or a framework lifecycle manager.
Also keep lifecycle responsibilities close together. The code that starts a listener should know how it is cancelled, awaited, and cleaned up. The code that closes a resource should stop the tasks that use it first.
Start with the complete traceback, fix the exception that occurred first, and then ensure the task that raised it is owned and observed through await, a task group, or a background-task supervisor.
Frequently Asked Questions
How do I find the cause of Task Exception Was Never Retrieved?
Find the exception immediately before the warning and the coroutine name after coro=<...>. The root cause is usually a TimeoutError, closed connection, browser-close error, or application exception inside that coroutine. Then locate where that coroutine was passed to asyncio.create_task() and make sure its result is awaited or collected.
Is Task Exception Was Never Retrieved safe to ignore?
No, not by default. It means a task failed and no code handled its result, so it can hide failed updates, lost messages, incomplete cleanup, or a real application bug. It may be harmless only after you have confirmed that the exception is an expected lifecycle event and your code deliberately retrieves and records it.
Why does try-except not always stop the warning?
A try/except only catches exceptions in the coroutine that reaches it. If a child coroutine was started with asyncio.create_task() and the parent never awaits that task, the child can fail independently and still produce the warning. Await the task, use asyncio.TaskGroup, or call task.result() from a supervised done callback.
What does page.set_default_timeout(500) mean in Playwright?
page.set_default_timeout(500) configures a 500-millisecond default timeout for Playwright operations associated with that page. It can make a page-close or route-handling race easier to reproduce in a test. The right production timeout is the one configured for your application’s browser workflow, not automatically 500 milliseconds.
