Python — RuntimeWarning: coroutine was never awaited
Diagnose RuntimeWarning coroutine never awaited, find call sites that invoke async functions without awaiting or scheduling, and close orphans deterministically.
Symptoms
- RuntimeWarning: coroutine 'refresh_cache' was never awaited appears at interpreter shutdown or test teardown
- The side effect the coroutine was supposed to produce (cache refresh, audit write, notification) never happens
- Flake8 or linters flag the same lines as RUF028 or W605-style unused expression issues
Possible causes
- An async function is called without await, creating a coroutine object that is immediately discarded
- A function was converted from sync to async and one caller was not updated
- fire-and-forget was intended but the coroutine was not wrapped in asyncio.create_task
- A conditional branch returns the coroutine object instead of awaiting it
Diagnosis steps
Read the coroutine name and line
python -W error::RuntimeWarning -m app.main 2>&1 | tail -5Interpretation
Promoting the warning to an error pinpoints the exact call site at the moment of invocation instead of at noisy shutdown.
Find bare invocations of async functions
grep -Rn "refresh_cache(\|notify(" src/ | grep -v "await\|create_task\|ensure_future"Interpretation
A call line without await or create_task is the orphan factory; the grep excludes legitimate schedulers.
Detect orphans programmatically
python -X dev -c "import asyncio, app.main" # dev mode reports unawaited coroutines with source linesInterpretation
The dev-mode report lists each orphan with its creation traceback, converting shutdown noise into an actionable list.
Root cause
Calling an async function does not run it; it constructs a coroutine object. Without await, create_task, or gather, the body never executes, and the interpreter raises RuntimeWarning when the garbage collector destroys the un-awaited coroutine.
Fix
Await the coroutine when the result matters; schedule it with asyncio.create_task and keep a reference when it is fire-and-forget; convert the function back to sync if no await exists inside it. Enable python -X dev or -W error::RuntimeWarning in CI to catch every future orphan.
Risk note
create_task without a stored reference can be garbage collected mid-flight, silently cancelling the work. Hold tasks in a set and discard on completion.
Verify the fix
Run the affected flows with RuntimeWarning as error and assert zero exceptions; verify the intended side effect (cache entry, row, notification) is observable.
Symptom walkthrough
The warning arrives when it is far too late, at garbage collection time:
RuntimeWarning: coroutine 'refresh_cache' was never awaited
Coroutine created at (most recent call last)
File "app/api.py", line 42, in handler
refresh_cache(user_id) <- called, never run
The created-at traceback is the diagnosis — it names the exact line that built the coroutine and dropped it. The business symptom pairs with it: whatever refresh_cache was supposed to do simply never occurred. Escalate the warning to an error in CI to catch this at build time:
python -W error::RuntimeWarning -m pytest
Fix in practice
Three resolutions by intent. The result matters — await it:
async function refresh() {
const data = await load();
cache.set(key, data);
}
await refresh();
Fire-and-forget is intentional — schedule it and keep the reference so the runtime cannot garbage-collect the task mid-flight:
const pending = new Set();
function fireAndForget(promise) {
pending.add(promise);
promise.finally(() => pending.delete(promise));
}
fireAndForget(refresh()); // runs to completion, tracked
The function contains no await at all — make it synchronous again and delete the async keyword; an async wrapper around synchronous work creates coroutine objects for no benefit. Then keep the CI guard on so the class cannot return: python -W error::RuntimeWarning -m pytest fails the build on the next orphan.