cocoindex-io/cocoindex · error · RuntimeError

coco.use_state() cannot be called inside a memoized function

Error message

coco.use_state() cannot be called inside a memoized function

What it means

Memoized functions (@coco.fn(memo=True)) must be pure and replayable: their result depends only on inputs and code. use_state() introduces persistent component state, which breaks memoization semantics, so calling it inside a memoized function is disallowed.

Source

Thrown at python/cocoindex/_internal/api.py:867

        # Typed — handle.value is Cursor, with full type inference
        @dataclass
        class Cursor:
            pos: int
            tag: str

        cur = coco.use_state("cursor", type_hint=Cursor, initial_value=Cursor(0, "init"))
        cur.value.pos += 1
        cur.value = Cursor(cur.value.pos, "next")
    """
    ctx = get_context_from_ctx()
    if ctx._core_path != ctx._core_processor_ctx.stable_path:
        raise RuntimeError(
            "coco.use_state() cannot be called inside a `with coco.component_subpath()` block"
        )

    if ctx._in_memo_fn:
        raise RuntimeError(
            "coco.use_state() cannot be called inside a memoized function"
        )
    try:
        # initial_value passed unserialized; engine core drops it if a value is
        # already stored on the previous run for this key.
        stored = ctx._core_processor_ctx.use_state(key, initial_value)
    except ValueError as e:
        # Rust client errors surface as ValueError; normalize to RuntimeError so
        # all use_state usage errors have a consistent type for callers.
        raise RuntimeError(str(e)) from None
    if type_hint is not None:
        deserializer = get_deserialize_fn(
            type_hint,  # type: ignore[arg-type]  # type objects are hashable at runtime
            source_label=f"use_state key {key!r}",
        )
    else:
        deserializer = _DESERIALIZE_ANY
    return StateHandle(key, stored, deserializer, ctx._core_processor_ctx)

View on GitHub (pinned to e84aa99b32)

Solutions

  1. Remove memo=True from the function so it can hold component state
  2. Move the use_state() call out of the memoized function into the owning component body
  3. Pass the state value into the memoized function as a plain argument instead

Example fix

// before
@coco.fn(memo=True)
async def step(ctx):
    cur = coco.use_state("cursor", initial_value=0)
// after
async def component_main(ctx):
    cur = coco.use_state("cursor", initial_value=0)
    await step(ctx, cur.value)
Defensive patterns

Strategy: validation

Validate before calling

# never call coco.use_state inside @coco.fn(memo=True) functions
# pass state values in as arguments instead:
async def step(ctx, cursor): ...

Try / catch

try:
    cur = coco.use_state("k", initial_value=0)
except RuntimeError as e:
    if "memoized function" in str(e):
        raise  # move state to the owning component and pass it in

Prevention

When it happens

Trigger: Calling coco.use_state(...) from within a function decorated with @coco.fn(memo=True) (detected via ctx._in_memo_fn).

Common situations: Adding state to a memoized helper when converting it into a component; copy-pasting component-body code (with use_state) into a memoized function; misunderstanding memo=True as 'cache the state too'.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of cocoindex-io/cocoindex@e84aa99b32 (2026-09-08). Data as JSON: /api/errors/2e63cd380588e051. Report an issue: GitHub.