reflex-dev/reflex · error · ImmutableStateError

The state is already mutable. Do not nest `async with self`

Error message

The state is already mutable. Do not nest `async with self` blocks.

What it means

Inside a background (@rx.event(background=True)) task, state mutability is guarded by an async context lock held by the current task. Re-entering `async with self` while the same task already holds the lock is a programming error (the state is already mutable), so ImmutableStateError is raised instead of deadlocking on a non-reentrant lock.

Source

Thrown at reflex/istate/proxy.py:148

            parent_state = (
                await self._self_parent_state_proxy.__aenter__()
            ).__wrapped__
            super().__setattr__(
                "__wrapped__",
                await parent_state.get_state(
                    State.get_class_substate(self._self_substate_path)
                ),
            )
            self._self_entered_context = True
            return self
        current_task = asyncio.current_task()
        if (
            self._self_actx_lock.locked()
            and current_task == self._self_actx_lock_holder
        ):
            msg = "The state is already mutable. Do not nest `async with self` blocks."
            raise ImmutableStateError(msg)

        ctx = EventContext.get()

        await self._self_actx_lock.acquire()
        try:
            self._self_actx_lock_holder = current_task
            self._self_actx = ctx.state_manager.modify_state_with_links(
                token=self._self_substate_token, event=self._self_event
            )
            mutable_state = await self._self_actx.__aenter__()
            self._self_mutable = True
            self._self_entered_context = True
            super().__setattr__(
                "__wrapped__", mutable_state.get_substate(self._self_substate_path)
            )
        except (Exception, asyncio.CancelledError):
            # Restore the proxy to a consistent state since __aexit__ will not be called when __aenter__ raises.
            await self.__aexit__(*sys.exc_info())

View on GitHub (pinned to 45b8ed5ab7)

Solutions

  1. Flatten to a single `async with self` block per task and do all mutations inside it
  2. Make helpers take the already-mutable state (call them inside the context) rather than opening their own
  3. If separate mutation windows are genuinely needed, exit the first context before entering the second

Example fix

# before
async with self:
    self.a = 1
    async with self:  # ImmutableStateError
        self.b = 2

# after
async with self:
    self.a = 1
    self.b = 2
Defensive patterns

Strategy: type-guard

Prevention

When it happens

Trigger: Nesting `async with self:` blocks inside one background event handler, e.g. calling a helper that also does `async with self` from inside an existing `async with self` block in the same task.

Common situations: Refactoring background handlers into helper functions that each open their own context; copy-pasting a mutation block into an already-mutable region; awaiting a utility coroutine that internally enters the context.

Related errors


AI-assisted analysis of reflex-dev/reflex@45b8ed5ab7 (2026-08-28). Data as JSON: /api/errors/28de64e72132345c. Report an issue: GitHub.