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
- Create a new cursor for each unit of work instead of reusing closed ones: cur = conn.cursor()
- Finish all fetches before calling close(); treat close() as the last operation on the object
- Restructure so each owner creates, uses, and closes its own cursor within one scope
- 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
- One cursor per unit of work; create, use, close in the same scope
- Never store cursors on long-lived objects (handlers, services) for later reuse
- Close is terminal: complete all fetches first, including exhausting iterators
- Cursor has no context manager — use try/finally to guarantee close ordering
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
- Cannot operate on a closed cursor
- Cannot operate on a closed cursor
- Cannot operate on a closed connection
- Cannot operate on a closed connection
- Database is closed
AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16).
Data as JSON: /api/errors/d9d1825d866dabb4.
Report an issue: GitHub.