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
- Use 'async with lock:' instead of manual acquire/release
- Ensure release() is only called on the success path of acquire()
- 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
- Always prefer 'async with lock' over manual acquire/release
- Track ownership when passing locks across tasks
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
- Can not create a worker from a non-async function unless `th
- unable to run callback {event.callback!r}; {error}
- Timed out while waiting for widgets to process pending messa
- Can't await screen.dismiss() from the screen's message handl
- Request to run a non-async function as an async worker
AI-assisted analysis of Textualize/textual@06dbeef4bb (2026-08-27).
Data as JSON: /api/errors/1f9501ab75247f54.
Report an issue: GitHub.