Textualize/textual · error · RuntimeError

RLock.release called too many times

Error message

RLock.release called too many times

What it means

The reentrant async lock detected a release() without a matching acquire() (its internal count went negative). This indicates unbalanced lock usage, typically releasing from a different task or releasing twice.

Source

Thrown at src/textual/rlock.py:30

        self._lock = Lock()

    async def acquire(self) -> None:
        """Wait until the lock can be acquired."""
        task = current_task()
        assert task is not None
        if self._owner is None or self._owner is not task:
            await self._lock.acquire()
            self._owner = task
        self._count += 1

    def release(self) -> None:
        """Release a previously acquired lock."""
        task = current_task()
        assert task is not None
        self._count -= 1
        if self._count < 0:
            # Should not occur if every acquire as a release
            raise RuntimeError("RLock.release called too many times")
        if self._owner is task:
            if not self._count:
                self._owner = None
                self._lock.release()

    @property
    def is_locked(self):
        """Return True if lock is acquired."""
        return self._lock.locked()

    async def __aenter__(self) -> None:
        """Asynchronous context manager to acquire and release lock."""
        await self.acquire()

    async def __aexit__(self, _type, _value, _traceback) -> None:
        """Exit the context manager."""
        self.release()

View on GitHub (pinned to 06dbeef4bb)

Solutions

  1. Use 'async with lock:' instead of manual acquire/release
  2. Ensure release() is only called on the success path of acquire()
  3. Audit for double-release in exception handlers

Example fix

# before
await lock.acquire()
try:
    ...
finally:
    lock.release()  # may double-release
# after
async with lock:
    ...
Defensive patterns

Strategy: fallback

Try / catch

try:
    lock.release()
except RuntimeError:
    pass  # already released

Prevention

When it happens

Trigger: Calling release() on an RLock more times than acquire() succeeded, or releasing in a finally block after acquire raised/timed out.

Common situations: Manual acquire/release in try blocks where acquire failed but release still ran, or copying release calls across code paths.

Related errors


AI-assisted analysis of Textualize/textual@06dbeef4bb (2026-08-27). Data as JSON: /api/errors/1f9501ab75247f54. Report an issue: GitHub.