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

  1. Create a cursor per operation: `cur = await conn.cursor()`, use it fully, then close
  2. Await all fetches before closing; treat close as terminal
  3. Route cursor usage through a single owner coroutine to prevent concurrent close/use
  4. 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

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


AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16). Data as JSON: /api/errors/b70112061373dd2a. Report an issue: GitHub.