tursodatabase/turso · error · ProgrammingError
Cannot operate on a closed cursor
Error message
Cannot operate on a closed cursor
What it means
The async Cursor wrapper mirrors the blocking cursor's lifecycle: every operation (execute, fetchone, fetchall, fetchmany, executemany, ...) calls _ensure_open, which raises ProgrammingError once _closed is True. The wrapper closes when you await cur.close() or when the underlying blocking cursor was closed via the connection.
Source
Thrown at bindings/python/turso/lib_aio.py:307
self._ensure_open()
def _many() -> list[Any]:
n = self.arraysize if size is None else size
return list(self._bcursor.fetchmany(n)) # type: ignore[union-attr]
return await self._connection._run(_many)
async def fetchall(self) -> list[Any]:
self._ensure_open()
def _all() -> list[Any]:
return list(self._bcursor.fetchall()) # type: ignore[union-attr]
return await self._connection._run(_all)
def _ensure_open(self) -> None:
if self._closed:
raise ProgrammingError("Cannot operate on a closed cursor")
# Properties reflecting DB-API attributes of the last executed statement
@property
def description(self) -> tuple[tuple[str, None, None, None, None, None, None], ...] | None:
return self._description
@property
def lastrowid(self) -> int | None:
return self._lastrowid
@property
def rowcount(self) -> int:
return self._rowcount
# Make cursor usable as async context manager, similar to aiosqlite
async def __aenter__(self) -> "Cursor":
return self
View on GitHub (pinned to bad083fafb)
Solutions
- Create a cursor per operation: `cur = await conn.cursor()`, use it fully, then close
- Await all fetches before closing; treat close as terminal
- Route cursor usage through a single owner coroutine to prevent concurrent close/use
- Catch ProgrammingError with this message as a lifecycle-bug signal and recreate the cursor
Example fix
# before
cur = await conn.cursor()
await cur.execute("SELECT id FROM t")
await cur.close()
rows = await cur.fetchall() # ProgrammingError
# after
cur = await conn.cursor()
try:
await cur.execute("SELECT id FROM t")
rows = await cur.fetchall()
finally:
await cur.close() Defensive patterns
Strategy: try-catch
Try / catch
try:
rows = await cur.fetchall()
except ProgrammingError as e:
if "closed cursor" in str(e):
cur = await conn.cursor() # recreate and re-run the statement once
await cur.execute(last_sql, last_params)
rows = await cur.fetchall()
else:
raise Prevention
- Await every fetch before awaiting cur.close(); close is terminal
- Create a cursor per operation instead of caching one across request cycles
- Keep cursor usage single-owner: one coroutine creates, uses, and closes it
- Wrap cursor lifecycles in try/finally so errors don't leave half-consumed cursors around
When it happens
Trigger: `await cur.fetchall()` after `await cur.close()`; awaiting cursor operations after the parent connection was closed; reusing a cursor cached on a request/service object across request cycles; a fetch task resuming after close.
Common situations: Async web handlers caching cursors; background consumers sharing a cursor reference; code where one coroutine closes the cursor while another still awaits results from it.
Related errors
- Cannot operate on a closed cursor
- Cannot operate on a closed connection
- Cannot operate on a closed cursor
- Cannot operate on a closed connection
- Database is closed
AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16).
Data as JSON: /api/errors/b70112061373dd2a.
Report an issue: GitHub.