Fix 'RuntimeWarning: coroutine was never awaited' in Python async/await

beginner🐍 Python2026-03-25| Python 3.4+, all operating systems (Windows, macOS, Linux), any project using asyncio or async/await syntax

Error Message

RuntimeWarning: coroutine 'xxx' was never awaited
#python#asyncio#coroutine#async#await#runtimewarning

The Error Scenario

You add async def to a function, call it, and Python throws this at runtime:

RuntimeWarning: coroutine 'fetch_data' was never awaited
RuntimeWarning: Enable tracemalloc to get the object allocation traceback

Your function never ran. No data, no side effects β€” nothing. The call returned a coroutine object and Python silently discarded it.

This trips up almost everyone learning async Python for the first time.

Why This Happens

Calling an async def function does not execute it. It returns a coroutine object β€” a suspended computation sitting in memory, waiting to be scheduled. Nothing runs until something awaits it.

Two situations trigger this warning:

  • Calling an async function without await inside another async function
  • Calling an async function from regular synchronous code without asyncio.run()

Example that causes the error

import asyncio

async def fetch_data():
    await asyncio.sleep(1)
    return "data"

async def main():
    result = fetch_data()   # BUG: missing await
    print(result)           # prints: <coroutine object fetch_data at 0x...>

asyncio.run(main())

Instead of "data", you get a coroutine object printed to the console. Python also emits:

RuntimeWarning: coroutine 'fetch_data' was never awaited

The Fix: Add await

Put await in front of every async function call inside an async context. One keyword, problem solved:

import asyncio

async def fetch_data():
    await asyncio.sleep(1)
    return "data"

async def main():
    result = await fetch_data()   # FIXED
    print(result)                 # prints: data

asyncio.run(main())

Calling Async Code from Synchronous Code

In a regular (non-async) function, you can't use await. Use asyncio.run() instead β€” it creates a new event loop, runs your coroutine to completion, and returns the result:

import asyncio

async def fetch_data():
    await asyncio.sleep(1)
    return "data"

def main():  # regular sync function
    result = asyncio.run(fetch_data())   # correct
    print(result)

main()

One important caveat: never call asyncio.run() inside an already-running event loop β€” for example, inside another async function, a Jupyter notebook cell, or a FastAPI request handler. You'll get a different error:

RuntimeError: This event loop is already running

In those environments, just use await directly.

Running Multiple Coroutines Concurrently

Need to fire off several async functions at once? Use asyncio.gather(). It runs all of them concurrently and collects the results:

import asyncio

async def task_a():
    await asyncio.sleep(1)
    return "A done"

async def task_b():
    await asyncio.sleep(1)
    return "B done"

async def main():
    # Both tasks run in parallel β€” total wall time ~1 second, not 2
    results = await asyncio.gather(task_a(), task_b())
    print(results)  # ['A done', 'B done']

asyncio.run(main())

Skip the await and you're back to square one β€” two coroutine objects created, never executed, warning emitted.

The Class Method Trap

Python's __init__ method cannot be async. This catches people off guard when setting up objects that need async initialization:

class DataFetcher:
    async def load(self):
        await asyncio.sleep(0.5)
        self.data = "loaded"

# Wrong β€” __init__ can't be async, so you can't await here
class App:
    def __init__(self):
        fetcher = DataFetcher()
        fetcher.load()  # coroutine never awaited!

# Right β€” move async setup into a dedicated async method
class App:
    async def setup(self):
        fetcher = DataFetcher()
        await fetcher.load()
        self.fetcher = fetcher

async def main():
    app = App()
    await app.setup()

asyncio.run(main())

Finding the Exact Line with tracemalloc

In a large codebase, the warning alone won't tell you where the unawaited coroutine was created. Enable tracemalloc at the top of your script and Python will include the exact file and line number:

import tracemalloc
tracemalloc.start()

import asyncio

async def fetch_data():
    return "data"

async def main():
    fetch_data()  # not awaited

asyncio.run(main())

The warning output now shows the allocation traceback β€” much more useful than hunting through hundreds of lines of code.

Make It a Hard Error During Development

Warnings are easy to miss, especially when buried in test output. Promote this one to a hard crash so it fails loudly:

import warnings
warnings.filterwarnings("error", category=RuntimeWarning)

Or pass the flag directly to Python:

python -W error::RuntimeWarning your_script.py

Now any unawaited coroutine raises an exception and stops execution immediately. CI will catch it before it reaches production.

Verifying the Fix

  • Re-run your script. The RuntimeWarning line should be gone.
  • Confirm the async function's return value is actual data, not a <coroutine object ...> string.
  • Add a quick assertion in your test suite:
import asyncio

async def fetch_data():
    return "data"

async def test_fetch():
    result = await fetch_data()
    assert result == "data", f"Expected 'data', got {result!r}"
    print("Test passed")

asyncio.run(test_fetch())

Quick Reference

  • async β†’ async: result = await func()
  • sync β†’ async: result = asyncio.run(func())
  • Multiple concurrent tasks: results = await asyncio.gather(func1(), func2())
  • Can't find which line? Add tracemalloc.start() at the top of your script
  • In CI/tests: run with -W error::RuntimeWarning to turn it into a hard crash

Related Error Notes