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
awaitinside 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
RuntimeWarningline 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::RuntimeWarningto turn it into a hard crash

