← Debugging Guides

阅读中文版本

Python — RuntimeError: asyncio.run() cannot be called from a running event loop

Resolve asyncio.run() cannot be called from a running event loop in notebooks and frameworks, find the outer loop owner, and await coroutines directly instead.

beginnerdebuggingpythonasynciojupyter

Symptoms

  • RuntimeError: asyncio.run() cannot be called from a running event loop when calling a library function from Jupyter or inside FastAPI handlers
  • The same code runs fine as a plain python script but fails in a notebook or web framework
  • The stack shows the failing asyncio.run nested inside a framework-managed handler

Possible causes

  • Jupyter already runs an event loop in the notebook kernel, so asyncio.run tries to nest a second loop
  • FastAPI or another async framework owns the loop that invokes the code, and a sync helper calls asyncio.run
  • A library exposes a sync wrapper around async code and calls asyncio.run internally when already inside a loop
  • Test runners with event loop fixtures re-enter async code through sync helpers

Diagnosis steps

Confirm a loop is already active

python -c "import asyncio; print(asyncio.get_running_loop())" # inside the failing context

Interpretation

A returned loop object proves an outer loop exists; the error is the guard against nesting, not a broken install.

Identify the loop owner

python -c "import asyncio; loop=asyncio.get_running_loop(); print(loop, asyncio.current_task())"

Interpretation

The current task and its name usually reveal the framework integration that owns execution.

Locate nested run calls

grep -Rn "asyncio.run(\|get_event_loop().run_until_complete" src/ | head

Interpretation

Call sites reachable from async contexts are the re-entry points to refactor.

Root cause

asyncio.run creates a new event loop, runs the coroutine to completion, and closes the loop. Calling it while another loop is already running would nest two loops in one thread, which asyncio forbids by raising the RuntimeError — the error is a correctness guard, not a malfunction.

Fix

Inside an async context, await the coroutine directly. For sync code that must call async code, offload to a worker thread with its own loop. In notebooks, either use top-level await or run blocking code in a thread executor. Prefer asyncio.run as a program entry point only.

Risk note

nest_asyncio patches the guard but is a monkeypatch that can interact badly with library internals and cleanup. Use it only as a last resort in notebooks.

Verify the fix

Invoke the previously failing code path from every context (notebook cell, framework handler, CLI) and assert completion without RuntimeError; confirm loop count stays one per thread.

Symptom walkthrough

The error text states the rule it enforces:

RuntimeError: asyncio.run() cannot be called from a
running event loop

The context asymmetry is the fingerprint: a plain python script works because no loop is running, while a notebook cell or a FastAPI handler fails because the framework already owns a loop in this thread. Verify the ownership from inside the failing context:

import asyncio
asyncio.get_running_loop()
# <_UnixSelectorEventLoop running=True closed=False debug=False>

running=True means the environment, not your code, is the host — and nesting a second loop is what the guard forbids.

Fix in practice

Match the call shape to the context you are in. Already async: await directly.

async def handler():
    result = await fetch_all(urls)   # no run() anywhere

Sync code that must drive async code: give it a private loop in a worker thread.

import asyncio, threading

def run_async(coro):
    result = {}
    def target():
        result["value"] = asyncio.run(coro)
    t = threading.Thread(target=target)
    t.start(); t.join()
    return result["value"]

Notebook cells: use top-level await (Jupyter supports it), or push blocking work to an executor thread. Reserve asyncio.run for genuine program entry points — the first sync frame of a CLI or script.