tursodatabase/turso · error · ProgrammingError

Cannot operate on a closed connection

Error message

Cannot operate on a closed connection

What it means

The async Connection in lib_aio wraps a blocking connection on a dedicated worker thread; every awaited operation is funneled through _run, which refuses to enqueue work once close() has completed (_closed = True). The result is a DB-API ProgrammingError("Cannot operate on a closed connection") raised from the awaiting coroutine.

Source

Thrown at bindings/python/turso/lib_aio.py:92

    def __await__(self):
        async def _await_open() -> "Connection":
            await self._open_future
            return self

        return _await_open().__await__()

    async def __aenter__(self) -> "Connection":
        await self
        return self

    async def __aexit__(self, exc_type, exc, tb) -> None:
        # Just close the connection - do not add any extra logic
        await self.close()

    # Internal helper: schedule a callable to run in the worker thread and await its result.
    async def _run(self, func: Callable[[], Any]) -> Any:
        if self._closed:
            raise ProgrammingError("Cannot operate on a closed connection")
        fut = self._loop.create_future()
        self._queue.put_nowait((fut, func))
        return await fut

    # Internal helper: enqueue a callable but do not await completion (used for property setters).
    def _run_nowait(self, func: Callable[[], Any]) -> None:
        if self._closed:
            raise ProgrammingError("Cannot operate on a closed connection")
        fut = self._loop.create_future()
        self._queue.put_nowait((fut, func))

    # Cursor factory returning async Cursor wrapper
    def cursor(self, factory: Optional[Callable[[BlockingConnection], BlockingCursor]] = None) -> "Cursor":
        # Creation of the underlying blocking cursor is enqueued to preserve thread affinity.
        return Cursor(self, factory=factory)

    # Helpers similar to aiosqlite
    async def execute(self, sql: str, parameters: Sequence[Any] | Mapping[str, Any] = ()) -> "Cursor":

View on GitHub (pinned to bad083fafb)

Solutions

  1. Scope all awaited operations inside the connection's lifetime: `async with turso.connect_aio(...) as conn: ...`
  2. Cancel or drain background tasks that use the connection before awaiting close()
  3. If re-connecting, replace the reference first and route all operations through a single accessor that owns the current connection
  4. Catch ProgrammingError with this message at app boundaries to convert late work into a clean "shutting down" response

Example fix

# before
conn = await turso.connect_aio("db")
await conn.close()
await conn.execute("SELECT 1")  # ProgrammingError

# after
async with await turso.connect_aio("db") as conn:
    await conn.execute("SELECT 1")
# all uses stay inside the block
Defensive patterns

Strategy: try-catch

Try / catch

try:
    rows = await conn.execute("SELECT 1").fetchall()
except ProgrammingError as e:
    if "closed connection" in str(e):
        conn = await reconnect()  # shutdown race or stale reference: rebuild and retry
        rows = await conn.execute("SELECT 1").fetchall()
    else:
        raise

Prevention

When it happens

Trigger: `await conn.execute(...)` / `await conn.commit()` after `await conn.close()`; using the connection after its `async with` block exits; background tasks (queues, schedulers) still holding the connection when the app shuts down and closes it; concurrent close() while another coroutine is about to issue an operation (the check races with in-flight work by design — it fires before enqueue).

Common situations: Web handlers keeping a module-level connection closed on shutdown while late requests arrive; task cancellation paths that close the connection in finally; reconnection logic that closes the old connection before swapping references everywhere.

Related errors


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