RuntimeWarning: coroutine '...' was never awaited is one of the most misleading messages in asyncio: it fires when the orphaned coroutine is garbage-collected, so the traceback points at GC internals or an unrelated line, not the missing await. The coroutine never ran, so whatever side effect you expected silently did not happen. This guide turns that vague late warning into a hard error with a traceback that points straight at the coroutine's creation site, using tracemalloc and warning filters.
Prerequisites
- Python
3.8+(tracemalloc, the-X tracemallocflag, andwarningsfilters). - For mocking causes:
unittest.mock.AsyncMock(added in Python 3.8). - For the pytest path: any recent
pytestwithfilterwarningssupport.
Solution
Run the program with the warning promoted to an error and tracemalloc enabled so the message carries the allocation traceback:
# -W error::RuntimeWarning raises instead of logging late.
# -X tracemalloc attaches the traceback to where the coroutine was created.
python -W error::RuntimeWarning -X tracemalloc app.py
In code, the equivalent is explicit:
import asyncio, tracemalloc, warnings
tracemalloc.start() # record allocation tracebacks
warnings.simplefilter("error", RuntimeWarning) # missing await -> raised error
async def save(record: dict) -> None:
await asyncio.sleep(0) # pretend to persist
async def main() -> None:
save({"id": 1}) # BUG: no await -> coroutine created but never run
await asyncio.sleep(0) # yield so GC can collect the orphan and trigger the warning
asyncio.run(main())
With tracemalloc on, the raised error includes:
RuntimeWarning: coroutine 'save' was never awaited
Coroutine created at (most recent call last):
File "app.py", line 10, in main
save({"id": 1})
For the whole test suite, fail on it in pytest config:
# pyproject.toml
[tool.pytest.ini_options]
filterwarnings = ["error::RuntimeWarning"]
# run pytest with: pytest -W error::RuntimeWarning -p no:cacheprovider --tb=short
The fix is one of: add the await, schedule it with asyncio.create_task(save(...)), or include it in asyncio.gather(...).
Why this works
A coroutine object created by calling an async def does nothing until it is driven by an await or scheduled on the loop. If the only reference is dropped, the garbage collector reclaims it and CPython emits the RuntimeWarning from the coroutine's __del__ — which is why the default traceback is useless. tracemalloc records a traceback at every allocation, so when the warning fires asyncio can attach the creation traceback to it, pointing at the exact call site that forgot the await. Promoting the warning to an error with the warnings filter makes the failure deterministic and CI-visible instead of a line buried in logs after the program already produced wrong results.
Edge cases and failure modes
- Mocking an async method with
Mock. A plainMockreturns aMock, not an awaitable, so production code thatawaits it breaks, while test code that calls it without awaiting leaks a coroutine. Useunittest.mock.AsyncMockfor async methods — see when to use MagicMock vs Mock in Python. - Coroutine passed where a value is expected.
if save(record):is always truthy because the coroutine object is truthy; the body never persists anything. Await first, then test the result. - Fire-and-forget without a reference.
asyncio.create_task(coro)schedules the coroutine, but if you keep no reference the task can be GC'd mid-flight. Store the task and await it at shutdown. - Warning suppressed by a broad filter. A library or
pytestconfig withfilterwarnings = ["ignore"]hides it. Add an expliciterror::RuntimeWarningrule that takes precedence. - Late GC hides the origin without tracemalloc. Without
-X tracemallocthe warning has no creation traceback; always pair the two. The same allocation-traceback technique underpins memory profiling with tracemalloc.
Frequently Asked Questions
Why does the never-awaited warning point at the wrong line?
The RuntimeWarning fires when the unawaited coroutine is garbage collected, which can be far from where it was created. Enable tracemalloc so the warning includes the allocation traceback pointing at the coroutine's real origin.
How do I make a missing await fail the test suite?
Run with -W error::RuntimeWarning, or set filterwarnings = error::RuntimeWarning in pytest config, so the warning is raised as an error. Combine it with -X tracemalloc to get the allocation traceback.
What are the most common causes of an unawaited coroutine?
Calling an async function without await, passing a coroutine where a value is expected, forgetting to await asyncio.sleep or a client call, and mocking an async method with a plain Mock instead of AsyncMock.
← Back to Debugging Async Code and Event Loops