Debugging & Performance

Tracing "coroutine was never awaited" Warnings

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 tracemalloc flag, and warnings filters).
  • For mocking causes: unittest.mock.AsyncMock (added in Python 3.8).
  • For the pytest path: any recent pytest with filterwarnings support.

Solution

Run the program with the warning promoted to an error and tracemalloc enabled so the message carries the allocation traceback:

Bash
# -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:

Python
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:

Plain text
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:

TOML
# 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 plain Mock returns a Mock, not an awaitable, so production code that awaits it breaks, while test code that calls it without awaiting leaks a coroutine. Use unittest.mock.AsyncMock for 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 pytest config with filterwarnings = ["ignore"] hides it. Add an explicit error::RuntimeWarning rule that takes precedence.
  • Late GC hides the origin without tracemalloc. Without -X tracemalloc the 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