cocoindex-io/cocoindex · error · RuntimeError
Memo state function returned an awaitable from a sync contex
Error message
Memo state function returned an awaitable from a sync context with a running event loop. Use @coco.fn.as_async for the decorated function instead.
What it means
resolve_awaitables_sync is the sync/async bridge that blocks on awaitables returned by memo state functions via asyncio.run. It can only do this when no event loop is already running in the current thread; otherwise asyncio.run would fail. When it detects a running loop, it raises a RuntimeError whose message tells the caller to make the decorated function async (via @coco.fn.as_async) instead of returning an awaitable from a sync function.
Source
Thrown at python/cocoindex/_internal/context_keys.py:60
the call returned a mix of values and awaitables, and we must block the
caller until all awaitables resolve — but only if we're not already inside
a running event loop, in which case we raise with a caller-specific
message.
*running_loop_error_msg* is the ``RuntimeError`` message used when we
detect a running event loop — callers supply a message that points at
their own remediation (e.g. ``@coco.fn.as_async`` for per-call state fns,
or "provide the value outside an async context" for ``provide()``).
"""
awaitable_indices = [i for i, o in enumerate(items) if isinstance(o, Awaitable)]
if not awaitable_indices:
return items
try:
asyncio.get_running_loop()
except RuntimeError:
pass
else:
raise RuntimeError(running_loop_error_msg)
async def _gather() -> list[Any]:
return list(await asyncio.gather(*(items[i] for i in awaitable_indices)))
resolved = asyncio.run(_gather())
out = list(items)
for idx, val in zip(awaitable_indices, resolved):
out[idx] = val
return out
def _compute_initial_context_states(
state_fns: list[StateFnEntry], key_name: str
) -> list[Any]:
"""Call each state function with ``NON_EXISTENCE`` and return their states.
This is the one-time initial-state collection at ``provide()`` time. The
resulting states are cached on the :class:`ContextProvider` and reused onView on GitHub (pinned to e84aa99b32)
Solutions
- Make the decorated function an async function, or wrap the async implementation with the decorator's as_async form, e.g. `@coco.fn.as_async` instead of `@coco.fn`
- Run the memoized function from a sync context (blocking entry point like update_blocking) so no loop is running
- Change the state function to be synchronous so it does not return an awaitable
Example fix
// before
@coco.fn
def fetch_state(conn):
return conn.fetch_one("SELECT ...") # returns coroutine
// after
@coco.fn.as_async
async def fetch_state(conn):
return await conn.fetch_one("SELECT ...") Defensive patterns
Strategy: try-catch
Validate before calling
import asyncio
def safe_to_call_state_fn() -> bool:
try:
asyncio.get_running_loop()
return False
except RuntimeError:
return True Type guard
def is_awaitable(v: object) -> bool:
return isinstance(v, collections.abc.Awaitable) Try / catch
try:
result = memoized_fn(args)
except RuntimeError as e:
if "as_async" in str(e):
result = await memoized_fn_async(args) # async variant
else:
raise Prevention
- Use @coco.fn.as_async for any function that awaits async APIs
- Never mix sync functions that return coroutines with async entry points
- Check that no running event loop exists before calling sync memo state functions
When it happens
Trigger: Calling a memoized (@coco.fn) function (or providing a detect_change context key) from within a running event loop, where a sync state function returns a coroutine/awaitable. resolve_awaitables_sync is invoked from _compute_initial_context_states (provide() time) or _resolve_results_awaitables_sync (cache-hit validation) inside the loop, hits `asyncio.get_running_loop()` succeeding, and raises.
Common situations: Defining a @coco.fn-decorated state/context function whose body calls an async API but whose def is not `async def`, then invoking the memoized function inside an async app (App.update() etc.). Copying sync-style code from a blocking script into an async pipeline.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Cannot use sync 'with coco.runtime()' from within an async e
- coco.use_state() cannot be called inside a memoized function
- {type(self).__name__} cannot be used as a memoization key. T
- {type(obj).__name__} cannot be used as a memoization key. Th
- Unsupported type for memoization key: {type(obj)!r}. Provide
AI-assisted analysis of cocoindex-io/cocoindex@e84aa99b32 (2026-09-08).
Data as JSON: /api/errors/36380efbfcb95a36.
Report an issue: GitHub.