← Debugging Guides

阅读中文版本

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.

beginnerdebuggingpythonasynciowarnings

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 -5

Interpretation

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 lines

Interpretation

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.