tursodatabase/turso · error · ProgrammingError

Cannot operate on a closed cursor

Error message

Cannot operate on a closed cursor

What it means

Every Cursor method calls _ensure_open, which raises DB-API ProgrammingError once _closed is True. A cursor becomes closed after an explicit cursor.close(); any subsequent execute/fetch/description access fails. The check is on the cursor itself, so it fires even if the parent connection is still open.

Source

Thrown at bindings/python/turso/lib.py:565

    def close(self) -> None:
        if self._closed:
            return
        try:
            # Finalize any active statement to ensure completion.
            if self._active_stmt is not None:
                try:
                    self._active_stmt.finalize()
                except Exception:
                    pass
        finally:
            self._active_stmt = None
            self._active_has_rows = False
            self._closed = True

    def _ensure_open(self) -> None:
        if self._closed:
            raise ProgrammingError("Cannot operate on a closed cursor")

    @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

    def _reset_last_result(self) -> None:
        # Ensure any previous statement is finalized to not leak resources
        if self._active_stmt is not None:
            try:
                self._active_stmt.finalize()

View on GitHub (pinned to bad083fafb)

Solutions

  1. Create a new cursor for each unit of work instead of reusing closed ones: cur = conn.cursor()
  2. Finish all fetches before calling close(); treat close() as the last operation on the object
  3. Restructure so each owner creates, uses, and closes its own cursor within one scope
  4. Catch ProgrammingError with this message as a defensive signal of a lifecycle bug, then log and recreate

Example fix

# before
cur = conn.cursor()
cur.execute("SELECT id FROM t")
first = cur.fetchone()
cur.close()
rest = cur.fetchall()  # ProgrammingError: Cannot operate on a closed cursor

# after
cur = conn.cursor()
cur.execute("SELECT id FROM t")
rows = cur.fetchall()
cur.close()
Defensive patterns

Strategy: try-catch

Try / catch

try:
    rows = cur.fetchall()
except ProgrammingError as e:
    if "closed cursor" in str(e):
        cur = conn.cursor()          # lifecycle bug signal: recreate and retry once
        cur.execute(last_sql, last_params)
        rows = cur.fetchall()
    else:
        raise

Prevention

When it happens

Trigger: Calling execute()/fetchone()/fetchall()/executemany() after cur.close(); reusing a cursor stored on an object (request handler, service class) from a previous cycle; consuming an iterator after close; partial fetch then close then fetch again.

Common situations: Caching cursors for reuse instead of creating fresh ones per operation; cleanup code closing cursors while background tasks still hold references; loops that close inside the body but continue iterating.

Related errors


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