Bug
chainlit run fails on Python 3.14. The browser shows a white page because
every static-asset request (/assets/*.js, /public/*) returns HTTP 500:
anyio.NoEventLoopError: Not currently running on any asynchronous event loop.
Available async backends: asyncio, trio
Root cause
Corrected 2026-08-05. This section previously attributed the breakage to the loop=
keyword being removed from asyncio.ensure_future in 3.14
(bpo-39529). That attribution is incorrect: the
parameter is present and accepted on 3.14.1. The symptom, the failure chain and the fix
are unchanged.
chainlit/cli/__init__.py calls nest_asyncio.apply() at module import time. apply()
rebinds the global task and future classes to their pure-Python implementations, while
asyncio.current_task stays bound to the C accelerator, which reads a registry the
pure-Python Task never populates.
Verified on 3.14.1 with nest_asyncio==1.6.0:
before apply: asyncio.Task is <class '_asyncio.Task'>
after apply: asyncio.Task is <class 'asyncio.tasks.Task'>
_py_current_task(): <Task pending name='Task-1' …>
_c_current_task(): None
asyncio.current_task() is _c_current_task: True
asyncio.current_task() therefore returns None inside a running coroutine. anyio
indexes _task_states[host_task] — a WeakKeyDictionary — with that None:
TypeError: cannot create weak reference to 'NoneType' object
which surfaces as anyio.NoEventLoopError.
The failure chain:
chainlit run → import chainlit.cli → nest_asyncio.apply() at module level
apply() rebinds asyncio.Task/asyncio.Future to the pure-Python classes, while
current_task() keeps reading the C registry → returns None
- Browser requests
/assets/index-*.js
FileResponse.__call__ → anyio.to_thread.run_sync(os.stat, path)
anyio → sniffio.current_async_library() → asyncio.current_task() → None
anyio.NoEventLoopError → HTTP 500 → no JS → white page
The breakage is specific to 3.14 because nest_asyncio suspends the current task by
clearing asyncio.tasks._current_tasks, and 3.14 moved current-task tracking into the
thread state, making that suspension a no-op.
asyncio.ensure_future(..., loop=...) is not implicated. On 3.14.1 the signature is still
(coro_or_future, *, loop=None), the call succeeds, and nest_asyncio.apply() does not
raise:
ensure_future signature: (coro_or_future, *, loop=None)
ensure_future(loop=...) accepted: True
nest_asyncio.apply() raised: False
No release of nest_asyncio can fix this: the class rebind is the library's mechanism, not
a bug in it. The library has also had no release since 2023.
Fix
Remove nest_asyncio, and eliminate the reentrancy it was compensating for.
asyncio.run(start()) at the bottom of run_chainlit() is a top-level entry point, does
not run inside another event loop, and never needed a reentrant loop, so the apply() call
is dropped there.
Two call sites did depend on reentrancy:
WebsocketSession.get_config() called loop.run_until_complete() from inside the
running loop. It becomes an async def resolve_config() awaited from the async
connect() handler.
cl.run_sync() on the main thread while the loop is running drives the loop through a
new chainlit/_reentrant_loop.py, suspending the calling task around the nested run
rather than patching anything. asyncio.Task and asyncio.Future keep their C
implementations, so current_task() stays correct and anyio is unaffected.
Fix, updated pyproject.toml, regenerated lockfile and regression tests:
#2953
Related
Bug
chainlit runfails on Python 3.14. The browser shows a white page becauseevery static-asset request (
/assets/*.js,/public/*) returns HTTP 500:Root cause
chainlit/cli/__init__.pycallsnest_asyncio.apply()at module import time.apply()rebinds the global task and future classes to their pure-Python implementations, while
asyncio.current_taskstays bound to the C accelerator, which reads a registry thepure-Python
Tasknever populates.Verified on 3.14.1 with
nest_asyncio==1.6.0:asyncio.current_task()therefore returnsNoneinside a running coroutine.anyioindexes
_task_states[host_task]— aWeakKeyDictionary— with thatNone:which surfaces as
anyio.NoEventLoopError.The failure chain:
chainlit run→import chainlit.cli→nest_asyncio.apply()at module levelapply()rebindsasyncio.Task/asyncio.Futureto the pure-Python classes, whilecurrent_task()keeps reading the C registry → returnsNone/assets/index-*.jsFileResponse.__call__→anyio.to_thread.run_sync(os.stat, path)anyio→sniffio.current_async_library()→asyncio.current_task()→Noneanyio.NoEventLoopError→ HTTP 500 → no JS → white pageThe breakage is specific to 3.14 because
nest_asynciosuspends the current task byclearing
asyncio.tasks._current_tasks, and 3.14 moved current-task tracking into thethread state, making that suspension a no-op.
asyncio.ensure_future(..., loop=...)is not implicated. On 3.14.1 the signature is still(coro_or_future, *, loop=None), the call succeeds, andnest_asyncio.apply()does notraise:
No release of
nest_asynciocan fix this: the class rebind is the library's mechanism, nota bug in it. The library has also had no release since 2023.
Fix
Remove
nest_asyncio, and eliminate the reentrancy it was compensating for.asyncio.run(start())at the bottom ofrun_chainlit()is a top-level entry point, doesnot run inside another event loop, and never needed a reentrant loop, so the
apply()callis dropped there.
Two call sites did depend on reentrancy:
WebsocketSession.get_config()calledloop.run_until_complete()from inside therunning loop. It becomes an
async def resolve_config()awaited from the asyncconnect()handler.cl.run_sync()on the main thread while the loop is running drives the loop through anew
chainlit/_reentrant_loop.py, suspending the calling task around the nested runrather than patching anything.
asyncio.Taskandasyncio.Futurekeep their Cimplementations, so
current_task()stays correct andanyiois unaffected.Fix, updated
pyproject.toml, regenerated lockfile and regression tests:#2953
Related