{"record":{"id":"b70112061373dd2a","repo":"tursodatabase/turso","slug":"cannot-operate-on-a-closed-cursor-b70112","errorCode":null,"errorMessage":"Cannot operate on a closed cursor","messagePattern":"Cannot operate on a closed cursor","errorType":"exception","errorClass":"ProgrammingError","httpStatus":null,"severity":"error","filePath":"bindings/python/turso/lib_aio.py","lineNumber":307,"sourceCode":"        self._ensure_open()\n\n        def _many() -> list[Any]:\n            n = self.arraysize if size is None else size\n            return list(self._bcursor.fetchmany(n))  # type: ignore[union-attr]\n\n        return await self._connection._run(_many)\n\n    async def fetchall(self) -> list[Any]:\n        self._ensure_open()\n\n        def _all() -> list[Any]:\n            return list(self._bcursor.fetchall())  # type: ignore[union-attr]\n\n        return await self._connection._run(_all)\n\n    def _ensure_open(self) -> None:\n        if self._closed:\n            raise ProgrammingError(\"Cannot operate on a closed cursor\")\n\n    # Properties reflecting DB-API attributes of the last executed statement\n    @property\n    def description(self) -> tuple[tuple[str, None, None, None, None, None, None], ...] | None:\n        return self._description\n\n    @property\n    def lastrowid(self) -> int | None:\n        return self._lastrowid\n\n    @property\n    def rowcount(self) -> int:\n        return self._rowcount\n\n    # Make cursor usable as async context manager, similar to aiosqlite\n    async def __aenter__(self) -> \"Cursor\":\n        return self\n","sourceCodeStart":289,"sourceCodeEnd":325,"githubUrl":"https://github.com/tursodatabase/turso/blob/bad083fafbefdeae9a42ec19bdaaad8918dcf411/bindings/python/turso/lib_aio.py#L289-L325","documentation":"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.","triggerScenarios":"`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.","commonSituations":"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.","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"],"exampleFix":"# before\ncur = await conn.cursor()\nawait cur.execute(\"SELECT id FROM t\")\nawait cur.close()\nrows = await cur.fetchall()  # ProgrammingError\n\n# after\ncur = await conn.cursor()\ntry:\n    await cur.execute(\"SELECT id FROM t\")\n    rows = await cur.fetchall()\nfinally:\n    await cur.close()","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    rows = await cur.fetchall()\nexcept ProgrammingError as e:\n    if \"closed cursor\" in str(e):\n        cur = await conn.cursor()   # recreate and re-run the statement once\n        await cur.execute(last_sql, last_params)\n        rows = await cur.fetchall()\n    else:\n        raise","preventionTips":["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"],"tags":["python","asyncio","lifecycle","cursor","use-after-close"],"backgroundTag":"use-after-close","analyzedSha":"bad083fafbefdeae9a42ec19bdaaad8918dcf411","analyzedAt":"2026-08-16T23:12:11.798Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}