Python Async/Await Not Working ? 3 Silent Failures – And How to Fix Them

You aren’t bad at async. You’re just missing one rule.

You probably wrote something clean.

Python
async def fetch_data():
    print("Fetching...")
    return {"data": 42}

fetch_data()
print("Done")
Python

You expected "Fetching..." to appear. Maybe you even expected it to run in the background. Instead? Nothing printed. Or worse it printed "Done" first and your fetch never happened.

This isn’t a bug in Python. And it’s not because you don’t understand concurrency.

It’s because async/await works differently than a normal function call. And most tutorials skip the single most important rule:

async does nothing on its own. You have to drive it.

Let’s fix that. Below are the three real reasons your async code is broken and how to make it work without rewriting everything.


1. You called an async function, but forgot to await it (the most common trap)

Look at the example above. fetch_data() returns a coroutine object, not your actual data.

Python
>>> fetch_data()
<coroutine object fetch_data at 0x10a4b5c30>
Python

Python runs the first line of your function (the print), sees an await inside, and pauses. Then it hands you back a coroutine and says: “Come back when you’re ready to actually run this.”

If you never await that coroutine, it simply disappears when the script ends.

The fix:

Python
result = await fetch_data()   # Works inside an async function
Python

But wait you can’t just write await at the top level of a script (prior to Python 3.8+ you needed a workaround). That leads to our second problem.


2. You’re trying to run async code from a sync context (no event loop)

Async functions don’t run themselves. They need an event loop a thing that juggles tasks and says “okay, now you run, now you pause.”

When you call await fetch_data() inside another async function, you’re fine. The loop is already there.

But when you call it from normal synchronous code like a plain script or a Django view there’s no loop. So nothing runs.

The fix: Use asyncio.run() as your starting point.

Python
import asyncio

async def fetch_data():
    print("Fetching...")
    return {"data": 42}

# One line. Always do this at your script's entry point.
result = asyncio.run(fetch_data())
print(result)
Python

asyncio.run() creates a loop, runs your coroutine until it finishes, then cleans up. Without it, your async code is like a car with no ignition.

Pro tip for Jupyter / notebooks: They already run a loop. Use await fetch_data() directly. Never call asyncio.run() twice in one session—that’s a separate error (the “event loop is closed” nightmare).


3. You created tasks but didn’t await them (so they got cancelled)

Here’s where people feel betrayed. You thought you were being smart:

Python
async def slow_task():
    await asyncio.sleep(2)
    print("Slow done")

async def main():
    task = asyncio.create_task(slow_task())
    # Oh no. No await.
    print("Main finished")

asyncio.run(main())
Python

Output:

Main finished

Where’s "Slow done"? Gone. The moment main() finished, the event loop saw that task was still pending and said: “Script’s over, cancel everything.”

Python doesn’t wait for background tasks unless you explicitly tell it to.

Three ways to fix it (pick one):

  • Await the task directly (simplest):
    await task
  • Wait for a group of tasks (clean for many):
    await asyncio.gather(task1, task2)
  • Use asyncio.wait() with a timeout (advanced):
    done, pending = await asyncio.wait([task], timeout=5)

The golden rule: Every create_task() deserves an await somewhere later in that same function. Otherwise you’re firing and forgetting—and “forgetting” is the default.


A quick decision tree for “Why isn’t my async code running?”

Ask yourself in order:

  1. Did I forget await?
    → If you see <coroutine object...> in a print, yes.
  2. Am I calling an async function from normal sync code?
    → Wrap it in asyncio.run(your_func()).
  3. Did I create a task without awaiting or gathering it?
    → Add await task or await asyncio.gather(task).
  4. Is my event loop already running (Jupyter, Django Channels, FastAPI)?
    → Don’t use asyncio.run(). Use await directly or asyncio.create_task() and keep a reference.

If none of these apply, check if you’re accidentally blocking the loop with time.sleep() instead of await asyncio.sleep(). The first one freezes everything.


How to debug async failures without losing your mind

Most developers stare at async code like it’s a black box. Don’t do that. Use two simple techniques:

1. Add print statements with timestamps

Python
import time
print(f"[{time.time()}] Before await")
await something()
print(f"[{time.time()}] After await")
Python

You’ll see exactly where execution pauses or skips.

2. Use asyncio.current_task() to see what’s running

Python
import asyncio
print(asyncio.current_task())  # Shows you the task object
Python

If you get None, you have no loop. That’s your answer.


The bottom line

async/await isn’t broken. It’s just explicit. Python refuses to guess whether you want to run a function now, later, or in parallel.

Most of the time, your problem is one of three things:

  • You forgot await entirely
  • You forgot asyncio.run() at the top level
  • You created a task and walked away

Fix those, and async goes from “magic that fails” to “a tool that does exactly what you tell it to.”

Now go run your coroutines—for real this time.


What to read next (if you want to stop fighting Python)

  • asyncio.gather() vs asyncio.wait() – the short answer : use gather unless you need fine-grained control
  • How to timeout an async function without breaking everything (hint: asyncio.wait_for)
  • Why asyncio.run() is different from loop.run_until_complete() – and which one crashes your notebook
×