[{"data":1,"prerenderedAt":922},["ShallowReactive",2],{"page-\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-async-code-and-event-loops\u002Fdiagnosing-task-was-destroyed-warnings\u002F":3},{"id":4,"title":5,"body":6,"description":888,"extension":889,"meta":890,"navigation":117,"path":918,"seo":919,"stem":920,"__hash__":921},"content\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-async-code-and-event-loops\u002Fdiagnosing-task-was-destroyed-warnings\u002Findex.md","Diagnosing \"Task Was Destroyed but It Is Pending\"",{"type":7,"value":8,"toc":877},"minimark",[9,21,32,37,59,63,98,206,241,282,434,438,453,467,478,482,485,502,512,519,585,589,592,602,612,615,686,690,693,733,739,743,799,803,813,822,835,839,868,873],[10,11,12,16,17,20],"p",{},[13,14,15],"code",{},"Task was destroyed but it is pending!"," is one of asyncio's least helpful messages. It appears at the end of a test run, or when a service shuts down, or at some random moment when the garbage collector happens to run — usually nowhere near the code that created the task. It prints the task's repr, maybe a coroutine name, and nothing else. Its sibling, ",[13,18,19],{},"Task exception was never retrieved",", is worse: it means an error already happened, and nobody noticed.",[10,22,23,24,27,28,31],{},"Both have the same root: a task that nobody is responsible for. Either the code dropped its reference after ",[13,25,26],{},"create_task",", so the task is only weakly held and can be collected mid-flight, or the program ended without cancelling and awaiting background work. The fixes are structural — hold references, shut down deliberately, or use ",[13,29,30],{},"TaskGroup"," so ownership is automatic — and asyncio's debug mode turns the unhelpful message into one that says where the task came from.",[33,34,36],"h2",{"id":35},"prerequisites","Prerequisites",[38,39,40,51],"ul",{},[41,42,43,44,46,47,50],"li",{},"Python 3.11 or later (for ",[13,45,30],{},"), ",[13,48,49],{},"pytest-asyncio >= 0.23",".",[41,52,53,54,50],{},"Background from ",[55,56,58],"a",{"href":57},"\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-async-code-and-event-loops\u002F","Debugging async code and event loops",[33,60,62],{"id":61},"solution","Solution",[64,65,70],"pre",{"className":66,"code":67,"language":68,"meta":69,"style":69},"language-python shiki shiki-themes github-light github-dark","# Before — fire-and-forget: the only strong reference is dropped immediately.\nasync def handle(request):\n    asyncio.create_task(send_audit_event(request))    # may be collected mid-flight\n    return response\n","python","",[13,71,72,80,86,92],{"__ignoreMap":69},[73,74,77],"span",{"class":75,"line":76},"line",1,[73,78,79],{},"# Before — fire-and-forget: the only strong reference is dropped immediately.\n",[73,81,83],{"class":75,"line":82},2,[73,84,85],{},"async def handle(request):\n",[73,87,89],{"class":75,"line":88},3,[73,90,91],{},"    asyncio.create_task(send_audit_event(request))    # may be collected mid-flight\n",[73,93,95],{"class":75,"line":94},4,[73,96,97],{},"    return response\n",[64,99,101],{"className":66,"code":100,"language":68,"meta":69,"style":69},"# After (1) — hold references for background work that outlives a request.\n_background: set[asyncio.Task] = set()\n\ndef spawn(coro) -> asyncio.Task:\n    task = asyncio.create_task(coro)\n    _background.add(task)\n    task.add_done_callback(_background.discard)\n    task.add_done_callback(_log_failure)\n    return task\n\ndef _log_failure(task: asyncio.Task) -> None:\n    if not task.cancelled() and task.exception() is not None:\n        log.error(\"background task failed\", exc_info=task.exception())\n\nasync def shutdown() -> None:\n    for t in list(_background):\n        t.cancel()\n    await asyncio.gather(*_background, return_exceptions=True)\n",[13,102,103,108,113,119,124,130,136,142,148,154,159,165,171,177,182,188,194,200],{"__ignoreMap":69},[73,104,105],{"class":75,"line":76},[73,106,107],{},"# After (1) — hold references for background work that outlives a request.\n",[73,109,110],{"class":75,"line":82},[73,111,112],{},"_background: set[asyncio.Task] = set()\n",[73,114,115],{"class":75,"line":88},[73,116,118],{"emptyLinePlaceholder":117},true,"\n",[73,120,121],{"class":75,"line":94},[73,122,123],{},"def spawn(coro) -> asyncio.Task:\n",[73,125,127],{"class":75,"line":126},5,[73,128,129],{},"    task = asyncio.create_task(coro)\n",[73,131,133],{"class":75,"line":132},6,[73,134,135],{},"    _background.add(task)\n",[73,137,139],{"class":75,"line":138},7,[73,140,141],{},"    task.add_done_callback(_background.discard)\n",[73,143,145],{"class":75,"line":144},8,[73,146,147],{},"    task.add_done_callback(_log_failure)\n",[73,149,151],{"class":75,"line":150},9,[73,152,153],{},"    return task\n",[73,155,157],{"class":75,"line":156},10,[73,158,118],{"emptyLinePlaceholder":117},[73,160,162],{"class":75,"line":161},11,[73,163,164],{},"def _log_failure(task: asyncio.Task) -> None:\n",[73,166,168],{"class":75,"line":167},12,[73,169,170],{},"    if not task.cancelled() and task.exception() is not None:\n",[73,172,174],{"class":75,"line":173},13,[73,175,176],{},"        log.error(\"background task failed\", exc_info=task.exception())\n",[73,178,180],{"class":75,"line":179},14,[73,181,118],{"emptyLinePlaceholder":117},[73,183,185],{"class":75,"line":184},15,[73,186,187],{},"async def shutdown() -> None:\n",[73,189,191],{"class":75,"line":190},16,[73,192,193],{},"    for t in list(_background):\n",[73,195,197],{"class":75,"line":196},17,[73,198,199],{},"        t.cancel()\n",[73,201,203],{"class":75,"line":202},18,[73,204,205],{},"    await asyncio.gather(*_background, return_exceptions=True)\n",[64,207,209],{"className":66,"code":208,"language":68,"meta":69,"style":69},"# After (2) — structured concurrency when the work belongs to a scope.\nasync def process_batch(items):\n    async with asyncio.TaskGroup() as tg:\n        for item in items:\n            tg.create_task(process(item))\n    # All tasks finished, or all were cancelled and errors raised, before this line.\n",[13,210,211,216,221,226,231,236],{"__ignoreMap":69},[73,212,213],{"class":75,"line":76},[73,214,215],{},"# After (2) — structured concurrency when the work belongs to a scope.\n",[73,217,218],{"class":75,"line":82},[73,219,220],{},"async def process_batch(items):\n",[73,222,223],{"class":75,"line":88},[73,224,225],{},"    async with asyncio.TaskGroup() as tg:\n",[73,227,228],{"class":75,"line":94},[73,229,230],{},"        for item in items:\n",[73,232,233],{"class":75,"line":126},[73,234,235],{},"            tg.create_task(process(item))\n",[73,237,238],{"class":75,"line":132},[73,239,240],{},"    # All tasks finished, or all were cancelled and errors raised, before this line.\n",[64,242,246],{"className":243,"code":244,"language":245,"meta":69,"style":69},"language-bash shiki shiki-themes github-light github-dark","# Where was the leaked task created?\nPYTHONASYNCIODEBUG=1 pytest tests\u002Ftest_api.py -W error::pytest.PytestUnraisableExceptionWarning\n","bash",[13,247,248,254],{"__ignoreMap":69},[73,249,250],{"class":75,"line":76},[73,251,253],{"class":252},"sJ8bj","# Where was the leaked task created?\n",[73,255,256,260,264,268,272,275,279],{"class":75,"line":82},[73,257,259],{"class":258},"sVt8B","PYTHONASYNCIODEBUG",[73,261,263],{"class":262},"szBVR","=",[73,265,267],{"class":266},"sZZnC","1",[73,269,271],{"class":270},"sScJk"," pytest",[73,273,274],{"class":266}," tests\u002Ftest_api.py",[73,276,278],{"class":277},"sj4cs"," -W",[73,280,281],{"class":266}," error::pytest.PytestUnraisableExceptionWarning\n",[283,284,287,430],"figure",{"className":285},[286],"diagram",[288,289,296,297,296,301,296,305,296,329,296,337,296,347,296,354,296,360,296,367,296,372,296,377,296,381,296,385,296,389,296,397,296,402,296,405,296,410,296,413,296,415,296,419,296,422,296,427],"svg",{"viewBox":290,"role":291,"ariaLabelledBy":292,"xmlns":295},"0 0 800 232","img",[293,294],"td-t","td-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[298,299,300],"title",{"id":293},"How a pending task gets destroyed",[302,303,304],"desc",{"id":294},"create_task registers the task with the event loop, which holds it only weakly. If the calling code drops its reference, the garbage collector can destroy the task while it is still pending. If the code keeps a reference in a set or a TaskGroup, the task survives until it completes and is removed by a done callback.",[306,307,308,309,308,323,296],"defs",{},"\n    ",[310,311,318],"marker",{"id":312,"viewBox":313,"refX":314,"refY":315,"markerWidth":316,"markerHeight":316,"orient":317},"td-a","0 0 10 10","9","5","7","auto-start-reverse",[319,320],"path",{"d":321,"fill":322},"M0 0 L10 5 L0 10 z","#81b29a",[310,324,326],{"id":325,"viewBox":313,"refX":314,"refY":315,"markerWidth":316,"markerHeight":316,"orient":317},"td-b",[319,327],{"d":321,"fill":328},"#e07a5f",[330,331],"rect",{"x":332,"y":332,"width":333,"height":334,"rx":335,"fill":336},"0","800","232","14","#fffdf8",[338,339,346],"text",{"x":340,"y":341,"textAnchor":342,"fontSize":343,"fontWeight":344,"fill":345},"400","28","middle","15.5","700","#3d405b","The loop holds tasks weakly — someone else must hold them strongly",[330,348],{"x":349,"y":350,"width":351,"height":352,"rx":353,"fill":345},"26","100","160","54","10",[338,355,359],{"x":356,"y":357,"textAnchor":342,"fontSize":358,"fontWeight":344,"fill":336},"106","132","12","create_task()",[330,361],{"x":362,"y":363,"width":364,"height":352,"rx":353,"fill":365,"stroke":328,"strokeWidth":366},"300","50","200","#fbe9e3","2",[338,368,371],{"x":340,"y":369,"textAnchor":342,"fontSize":370,"fontWeight":344,"fill":345},"74","11.5","reference dropped",[338,373,376],{"x":340,"y":374,"textAnchor":342,"fontSize":375,"fill":345},"92","10.5","only weakly held",[330,378],{"x":362,"y":379,"width":364,"height":352,"rx":353,"fill":380,"stroke":322,"strokeWidth":366},"150","#e6f0ea",[338,382,384],{"x":340,"y":383,"textAnchor":342,"fontSize":370,"fontWeight":344,"fill":345},"174","held in set \u002F TaskGroup",[338,386,388],{"x":340,"y":387,"textAnchor":342,"fontSize":375,"fill":345},"192","strong reference",[75,390],{"x1":391,"y1":392,"x2":393,"y2":394,"stroke":328,"strokeWidth":395,"markerEnd":396},"188","118","296","82","1.6","url(#td-b)",[75,398],{"x1":391,"y1":399,"x2":393,"y2":400,"stroke":322,"strokeWidth":395,"markerEnd":401},"136","172","url(#td-a)",[330,403],{"x":404,"y":363,"width":364,"height":352,"rx":353,"fill":365,"stroke":328,"strokeWidth":366},"574",[338,406,409],{"x":407,"y":369,"textAnchor":342,"fontSize":370,"fontWeight":344,"fill":408},"674","#8f3d22","GC destroys it",[338,411,412],{"x":407,"y":374,"textAnchor":342,"fontSize":375,"fill":345},"\"destroyed but pending\"",[330,414],{"x":404,"y":379,"width":364,"height":352,"rx":353,"fill":380,"stroke":322,"strokeWidth":366},[338,416,418],{"x":407,"y":383,"textAnchor":342,"fontSize":370,"fontWeight":344,"fill":417},"#2a5f49","runs to completion",[338,420,421],{"x":407,"y":387,"textAnchor":342,"fontSize":375,"fill":345},"discarded when done",[75,423],{"x1":424,"y1":425,"x2":426,"y2":425,"stroke":328,"strokeWidth":395,"markerEnd":396},"502","77","570",[75,428],{"x1":424,"y1":429,"x2":426,"y2":429,"stroke":322,"strokeWidth":395,"markerEnd":401},"177",[431,432,433],"figcaption",{},"The asyncio documentation says it plainly: save a reference to the result of create_task, or the task may disappear mid-execution.",[33,435,437],{"id":436},"why-this-works","Why this works",[10,439,440,441,444,445,448,449,452],{},"The event loop keeps its set of all tasks in a ",[13,442,443],{},"WeakSet",", so the loop alone does not keep a task alive. While a task is actively scheduled — its next step queued with ",[13,446,447],{},"call_soon"," — the loop's ready queue holds a strong reference. While it is suspended waiting on a future, the future's callbacks hold one, as long as something holds the future. In the gaps, a task whose creator dropped the reference can have no strong references at all, and the garbage collector destroys it. Its ",[13,450,451],{},"__del__"," notices it was still pending and logs the warning.",[10,454,455,456,459,460,462,463,466],{},"The done-callback pattern closes the gap: the module-level set holds each task strongly until it completes, then ",[13,457,458],{},"discard"," removes it so the set does not grow forever. ",[13,461,30],{}," does the same internally and adds structured semantics — the ",[13,464,465],{},"async with"," block does not exit until every task in it has finished, so ownership is guaranteed by the code's shape.",[10,468,469,470,473,474,477],{},"At shutdown, the loop closes. Any task still pending at that point is destroyed with the same warning. ",[13,471,472],{},"asyncio.run"," cancels remaining tasks for you, but code that manages its own loop, or background tasks that ignore cancellation, can still leave pending tasks behind. An explicit ",[13,475,476],{},"shutdown()"," that cancels and awaits them makes the end of the program as deliberate as the start.",[33,479,481],{"id":480},"finding-the-leaking-task-in-a-test-suite","Finding the leaking task in a test suite",[10,483,484],{},"In a large suite, the warning often appears attributed to the wrong test — whichever one happened to trigger garbage collection. Three settings make it precise.",[10,486,487,490,491,494,495,497,498,501],{},[13,488,489],{},"PYTHONASYNCIODEBUG=1"," (or ",[13,492,493],{},"asyncio.run(..., debug=True)",") records a traceback at every ",[13,496,26],{}," call. The destroyed-task warning then includes ",[13,499,500],{},"source_traceback",", pointing at the line that created the task. That alone usually identifies the culprit.",[10,503,504,507,508,511],{},[13,505,506],{},"-W error::pytest.PytestUnraisableExceptionWarning"," makes pytest fail the test during which the warning surfaced. Combined with ",[13,509,510],{},"gc.collect()"," in an autouse fixture's teardown, the warning surfaces at the end of the test that leaked the task, not later.",[10,513,514,515,518],{},"Finally, ",[13,516,517],{},"pytest-asyncio","'s per-test event loops close at test end, so a background task spawned by one test and never awaited is destroyed at that test's teardown — which is the right place to report it.",[283,520,522,582],{"className":521},[286],[288,523,296,528,296,531,296,534,296,537,296,540,296,546,296,550,296,554,296,557,296,560,296,563,296,566,296,569,296,572,296,576,296,579],{"viewBox":524,"role":291,"ariaLabelledBy":525,"xmlns":295},"0 0 800 226",[526,527],"tdf-t","tdf-d",[298,529,530],{"id":526},"Pinning the warning to the right test",[302,532,533],{"id":527},"Three settings combine. Asyncio debug mode records where each task was created. A teardown fixture forces garbage collection so the warning appears at the end of the leaking test. Treating unraisable-exception warnings as errors turns it into a failure of that test with the creation traceback attached.",[330,535],{"x":332,"y":332,"width":333,"height":536,"rx":335,"fill":336},"226",[338,538,539],{"x":340,"y":341,"textAnchor":342,"fontSize":343,"fontWeight":344,"fill":345},"Three settings, one precise failure",[330,541],{"x":349,"y":542,"width":543,"height":379,"rx":358,"fill":544,"stroke":545,"strokeWidth":366},"52","236","#f7f0da","#f2cc8f",[338,547,489],{"x":548,"y":549,"textAnchor":342,"fontSize":358,"fontWeight":344,"fill":345},"144","80",[338,551,553],{"x":548,"y":552,"textAnchor":342,"fontSize":375,"fill":345},"112","records create_task",[338,555,556],{"x":548,"y":357,"textAnchor":342,"fontSize":375,"fill":345},"source traceback",[330,558],{"x":559,"y":542,"width":543,"height":379,"rx":358,"fill":380,"stroke":322,"strokeWidth":366},"282",[338,561,562],{"x":340,"y":549,"textAnchor":342,"fontSize":358,"fontWeight":344,"fill":345},"gc.collect() in teardown",[338,564,565],{"x":340,"y":552,"textAnchor":342,"fontSize":375,"fill":345},"warning fires at the end",[338,567,568],{"x":340,"y":357,"textAnchor":342,"fontSize":375,"fill":345},"of the leaking test",[330,570],{"x":571,"y":542,"width":543,"height":379,"rx":358,"fill":365,"stroke":328,"strokeWidth":366},"538",[338,573,575],{"x":574,"y":549,"textAnchor":342,"fontSize":358,"fontWeight":344,"fill":345},"656","-W error::Unraisable",[338,577,578],{"x":574,"y":552,"textAnchor":342,"fontSize":375,"fill":345},"warning becomes",[338,580,581],{"x":574,"y":357,"textAnchor":342,"fontSize":375,"fill":408},"a failure of that test",[431,583,584],{},"Together they turn an end-of-run message into a red test with a traceback pointing at the create_task line.",[33,586,588],{"id":587},"choosing-between-a-task-set-and-a-taskgroup","Choosing between a task set and a TaskGroup",[10,590,591],{},"Both fixes hold references; they differ in who owns the task's lifetime, and choosing the wrong one produces its own problems.",[10,593,594,595,597,598,601],{},"A ",[13,596,30],{}," ties tasks to a block of code. It is the right choice whenever the work has a natural end that the caller waits for: processing a batch, fanning out requests for one response, running a producer and consumer together until the queue drains. The block exits only when every task has finished, errors propagate as an ",[13,599,600],{},"ExceptionGroup",", and a failure in one task cancels its siblings. There is nothing to shut down later, because nothing outlives the block.",[10,603,604,605,608,609,611],{},"A task set with done callbacks is for work that deliberately outlives its caller: an audit event sent after the response has returned, a cache refresh triggered by a request, a long-running consumer started at application startup. The request handler cannot wait for these without defeating their purpose, so ownership moves to the application, and the application must cancel and await them at shutdown. Frameworks often provide a hook for this — a lifespan handler in Starlette and FastAPI, ",[13,606,607],{},"on_cleanup"," in aiohttp — and the ",[13,610,476],{}," function belongs there.",[10,613,614],{},"The mistake to avoid is using a set where a group fits. A request handler that spawns five tasks into a global set and returns without waiting leaves those tasks running after the response, with nobody reporting their errors to the caller. If the result matters to the response, use a group; if it does not, the set is fine, but log failures from the done callback so they are not lost.",[283,616,618,683],{"className":617},[286],[288,619,296,624,296,627,296,630,296,632,296,635,296,639,296,645,296,650,296,654,296,658,296,662,296,665,296,669,296,673,296,676,296,679],{"viewBox":620,"role":291,"ariaLabelledBy":621,"xmlns":295},"0 0 800 236",[622,623],"tds-t","tds-d",[298,625,626],{"id":622},"Scope-owned versus application-owned tasks",[302,628,629],{"id":623},"Two columns. TaskGroup: tasks owned by a code block, the caller waits, errors propagate, siblings are cancelled on failure; use for batches and fan-out. Task set: tasks owned by the application, the caller does not wait, failures are logged by a done callback, and tasks are cancelled and awaited at shutdown; use for background work that outlives a request.",[330,631],{"x":332,"y":332,"width":333,"height":543,"rx":335,"fill":336},[338,633,634],{"x":340,"y":341,"textAnchor":342,"fontSize":343,"fontWeight":344,"fill":345},"Who waits for the task decides the tool",[330,636],{"x":349,"y":363,"width":637,"height":638,"rx":358,"fill":380,"stroke":322,"strokeWidth":366},"360","164",[338,640,644],{"x":641,"y":642,"textAnchor":342,"fontSize":643,"fontWeight":344,"fill":345},"206","76","12.5","TaskGroup — scope owns it",[338,646,649],{"x":647,"y":356,"fontSize":648,"fill":345},"44","11","caller waits at end of block",[338,651,653],{"x":647,"y":652,"fontSize":648,"fill":345},"130","errors raise ExceptionGroup",[338,655,657],{"x":647,"y":656,"fontSize":648,"fill":345},"154","failure cancels siblings",[338,659,661],{"x":647,"y":660,"fontSize":648,"fontWeight":344,"fill":417},"190","batches, fan-out, pipelines",[330,663],{"x":664,"y":363,"width":637,"height":638,"rx":358,"fill":544,"stroke":545,"strokeWidth":366},"414",[338,666,668],{"x":667,"y":642,"textAnchor":342,"fontSize":643,"fontWeight":344,"fill":345},"594","task set — app owns it",[338,670,672],{"x":671,"y":356,"fontSize":648,"fill":345},"432","caller returns immediately",[338,674,675],{"x":671,"y":652,"fontSize":648,"fill":345},"done callback logs failures",[338,677,678],{"x":671,"y":656,"fontSize":648,"fill":345},"cancelled + awaited at shutdown",[338,680,682],{"x":671,"y":660,"fontSize":648,"fontWeight":344,"fill":681},"#8a5a00","audit events, refreshes, consumers",[431,684,685],{},"If the response depends on the result, the task belongs in a group; if not, in a set with a shutdown hook.",[33,687,689],{"id":688},"testing-that-shutdown-actually-cleans-up","Testing that shutdown actually cleans up",[10,691,692],{},"A shutdown routine that is never exercised tends to rot: a new background task is added without registering it, or a coroutine starts swallowing cancellation, and the warnings return. A focused test keeps it honest. Start the application in the test's event loop, trigger the code paths that spawn background work — a request that sends an audit event, a timer that schedules a refresh — then call the shutdown hook and assert that nothing is left:",[64,694,696],{"className":66,"code":695,"language":68,"meta":69,"style":69},"async def test_shutdown_leaves_no_tasks(app):\n    await app.startup()\n    await app.client.post(\"\u002Forders\", json=ORDER)\n    await app.shutdown()\n    current = asyncio.current_task()\n    leftover = [t for t in asyncio.all_tasks() if t is not current]\n    assert leftover == [], [t.get_coro() for t in leftover]\n",[13,697,698,703,708,713,718,723,728],{"__ignoreMap":69},[73,699,700],{"class":75,"line":76},[73,701,702],{},"async def test_shutdown_leaves_no_tasks(app):\n",[73,704,705],{"class":75,"line":82},[73,706,707],{},"    await app.startup()\n",[73,709,710],{"class":75,"line":88},[73,711,712],{},"    await app.client.post(\"\u002Forders\", json=ORDER)\n",[73,714,715],{"class":75,"line":94},[73,716,717],{},"    await app.shutdown()\n",[73,719,720],{"class":75,"line":126},[73,721,722],{},"    current = asyncio.current_task()\n",[73,724,725],{"class":75,"line":132},[73,726,727],{},"    leftover = [t for t in asyncio.all_tasks() if t is not current]\n",[73,729,730],{"class":75,"line":138},[73,731,732],{},"    assert leftover == [], [t.get_coro() for t in leftover]\n",[10,734,735,738],{},[13,736,737],{},"asyncio.all_tasks()"," returns every task not yet finished on the running loop. After a correct shutdown, only the test's own task remains. When the assertion fails, the message lists the leftover coroutines by name, which points straight at the spawner that forgot to register its task or the coroutine that refused to cancel. Run this test with debug mode enabled and the creation tracebacks are available too. It takes seconds to run and catches the regression at the moment it is introduced, rather than weeks later as a warning in someone's deployment logs.",[33,740,742],{"id":741},"edge-cases-and-failure-modes","Edge cases and failure modes",[38,744,745,762,770,784,793],{},[41,746,747,754,755,757,758,761],{},[748,749,750,751,50],"strong",{},"Tasks that swallow ",[13,752,753],{},"CancelledError"," A coroutine that catches ",[13,756,753],{}," and continues cannot be shut down; ",[13,759,760],{},"gather"," waits forever. Re-raise it after cleanup.",[41,763,764,769],{},[748,765,766,50],{},[13,767,768],{},"asyncio.shield"," A shielded inner task keeps running when the outer is cancelled and can outlive shutdown. Track and await shielded work explicitly.",[41,771,772,775,776,779,780,783],{},[748,773,774],{},"Libraries spawning tasks."," Some clients start background tasks (keepalives, reconnect loops) and expect ",[13,777,778],{},"close()"," or ",[13,781,782],{},"aclose()",". Use them as async context managers.",[41,785,786,792],{},[748,787,788,789,791],{},"Sync code calling ",[13,790,472],{}," repeatedly."," Each call creates and closes a loop; tasks spawned into one and not awaited are destroyed at its close.",[41,794,795,798],{},[748,796,797],{},"Never-retrieved exceptions."," The done callback in the solution logs failures immediately instead of at garbage collection.",[33,800,802],{"id":801},"frequently-asked-questions","Frequently Asked Questions",[10,804,805,808,809,812],{},[748,806,807],{},"What does \"Task was destroyed but it is pending!\" mean?","\nAn asyncio ",[13,810,811],{},"Task"," object was garbage collected, or the loop was closed, while the task had not finished. Either nothing held a reference to it, so it could be collected mid-flight, or the program shut down without cancelling and awaiting it.",[10,814,815,818,819,821],{},[748,816,817],{},"Why does asyncio need me to keep a reference to tasks?","\nThe event loop only keeps weak references to tasks. If your code drops the object returned by ",[13,820,26],{},", the task can be garbage collected before it completes, silently stopping its work.",[10,823,824,827,828,779,831,834],{},[748,825,826],{},"How is \"Task exception was never retrieved\" different?","\nThat task did finish, but with an exception, and nothing awaited it or called ",[13,829,830],{},"result()",[13,832,833],{},"exception()",". The exception is reported when the task is garbage collected, often far from where it happened.",[33,836,838],{"id":837},"related","Related",[38,840,841,847,854,861],{},[41,842,843,846],{},[55,844,845],{"href":57},"Debugging Async Code and Event Loops"," — asyncio debugging fundamentals.",[41,848,849,853],{},[55,850,852],{"href":851},"\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-async-code-and-event-loops\u002Ftracing-unawaited-coroutine-warnings\u002F","Tracing Unawaited Coroutine Warnings"," — the coroutine-level sibling.",[41,855,856,860],{},[55,857,859],{"href":858},"\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-async-code-and-event-loops\u002Ftracking-down-a-hung-await-with-task-stacks\u002F","Tracking Down a Hung await with Task Stacks"," — tasks that never finish.",[41,862,863,867],{},[55,864,866],{"href":865},"\u002Fsystematic-debugging-performance-profiling\u002Freading-tracebacks-and-exception-chains\u002Fgetting-useful-tracebacks-from-threads-and-tasks\u002F","Getting Useful Tracebacks from Threads and Tasks"," — surfacing background exceptions.",[10,869,870,871],{},"← Back to ",[55,872,845],{"href":57},[874,875,876],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":69,"searchDepth":82,"depth":82,"links":878},[879,880,881,882,883,884,885,886,887],{"id":35,"depth":82,"text":36},{"id":61,"depth":82,"text":62},{"id":436,"depth":82,"text":437},{"id":480,"depth":82,"text":481},{"id":587,"depth":82,"text":588},{"id":688,"depth":82,"text":689},{"id":741,"depth":82,"text":742},{"id":801,"depth":82,"text":802},{"id":837,"depth":82,"text":838},"Fix asyncio's \"Task was destroyed but it is pending!\" and \"Task exception was never retrieved\": keeping task references, shutting down cleanly, TaskGroup, and finding the leaking task in tests.","md",{"slug":891,"type":892,"breadcrumb":893,"datePublished":894,"dateModified":894,"faq":895,"howto":902},"diagnosing-task-was-destroyed-warnings","article","Task was destroyed","2026-09-18",[896,898,900],{"q":807,"a":897},"An asyncio Task object was garbage collected, or the loop was closed, while the task had not finished. Either nothing held a reference to it, so it could be collected mid-flight, or the program shut down without cancelling and awaiting it.",{"q":817,"a":899},"The event loop only keeps weak references to tasks. If your code drops the object returned by create_task, the task can be garbage collected before it completes, silently stopping its work.",{"q":826,"a":901},"That task did finish, but with an exception, and nothing awaited it or called result() or exception(). The exception is reported when the task is garbage collected, often far from where it happened.",{"name":903,"description":904,"steps":905},"How to fix Task was destroyed warnings","Keep references to background tasks, cancel and await them on shutdown, and prefer structured concurrency.",[906,909,912,915],{"name":907,"text":908},"Enable debug mode","Run with PYTHONASYNCIODEBUG=1 so the warning includes where the task was created.",{"name":910,"text":911},"Hold task references","Store tasks in a set and discard them in a done callback, or use a TaskGroup.",{"name":913,"text":914},"Cancel and await on shutdown","Cancel outstanding tasks and await them with gather(return_exceptions=True) before closing the loop.",{"name":916,"text":917},"Fail tests on the warning","Treat unraisable-exception warnings as errors so leaked tasks fail the test that caused them.","\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-async-code-and-event-loops\u002Fdiagnosing-task-was-destroyed-warnings",{"title":5,"description":888},"systematic-debugging-performance-profiling\u002Fdebugging-async-code-and-event-loops\u002Fdiagnosing-task-was-destroyed-warnings\u002Findex","wL77W3Qq9Ey6RM1wdcuQjontmCCW25e_eVAqKCJrwuQ",1789718769163]