{"record":{"id":"36380efbfcb95a36","repo":"cocoindex-io/cocoindex","slug":"memo-state-function-returned-an-awaitable-from-a-s","errorCode":null,"errorMessage":"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.","messagePattern":"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\\.","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"python/cocoindex/_internal/context_keys.py","lineNumber":60,"sourceCode":"    the call returned a mix of values and awaitables, and we must block the\n    caller until all awaitables resolve — but only if we're not already inside\n    a running event loop, in which case we raise with a caller-specific\n    message.\n\n    *running_loop_error_msg* is the ``RuntimeError`` message used when we\n    detect a running event loop — callers supply a message that points at\n    their own remediation (e.g. ``@coco.fn.as_async`` for per-call state fns,\n    or \"provide the value outside an async context\" for ``provide()``).\n    \"\"\"\n    awaitable_indices = [i for i, o in enumerate(items) if isinstance(o, Awaitable)]\n    if not awaitable_indices:\n        return items\n    try:\n        asyncio.get_running_loop()\n    except RuntimeError:\n        pass\n    else:\n        raise RuntimeError(running_loop_error_msg)\n\n    async def _gather() -> list[Any]:\n        return list(await asyncio.gather(*(items[i] for i in awaitable_indices)))\n\n    resolved = asyncio.run(_gather())\n    out = list(items)\n    for idx, val in zip(awaitable_indices, resolved):\n        out[idx] = val\n    return out\n\n\ndef _compute_initial_context_states(\n    state_fns: list[StateFnEntry], key_name: str\n) -> list[Any]:\n    \"\"\"Call each state function with ``NON_EXISTENCE`` and return their states.\n\n    This is the one-time initial-state collection at ``provide()`` time. The\n    resulting states are cached on the :class:`ContextProvider` and reused on","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/cocoindex-io/cocoindex/blob/e84aa99b3292c5270a4b313b2a7137ad9ce8ab3b/python/cocoindex/_internal/context_keys.py#L42-L78","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\n@coco.fn\ndef fetch_state(conn):\n    return conn.fetch_one(\"SELECT ...\")  # returns coroutine\n\n// after\n@coco.fn.as_async\nasync def fetch_state(conn):\n    return await conn.fetch_one(\"SELECT ...\")","handlingStrategy":"try-catch","validationCode":"import asyncio\n\ndef safe_to_call_state_fn() -> bool:\n    try:\n        asyncio.get_running_loop()\n        return False\n    except RuntimeError:\n        return True","typeGuard":"def is_awaitable(v: object) -> bool:\n    return isinstance(v, collections.abc.Awaitable)","tryCatchPattern":"try:\n    result = memoized_fn(args)\nexcept RuntimeError as e:\n    if \"as_async\" in str(e):\n        result = await memoized_fn_async(args)  # async variant\n    else:\n        raise","preventionTips":["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"],"tags":["python","asyncio","event-loop","memoization"],"backgroundTag":"invalid-state-transition","analyzedSha":"e84aa99b3292c5270a4b313b2a7137ad9ce8ab3b","analyzedAt":"2026-09-08T15:59:19.997Z","contentChangedAt":"2026-09-08T15:59:19.997Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}