# 34000 — invalid_cursor_name

> PostgreSQL SQLSTATE 34000: cursor lifecycle, session affinity, and recovery.
---

# 34000 — invalid_cursor_name

## At a glance {#at-a-glance}

`34000` means that a cursor or portal name cannot be resolved in the current backend session. A cursor that was closed, declared on another pooled connection, or ended with its transaction is not the same resource as a cursor with a bad position (`24000`).

<!-- BEGIN SQLSTATE FACTS: generated by scripts/generate.py; do not edit -->

| Field | Value |
| --- | --- |
| SQLSTATE | `34000` |
| Condition | `invalid_cursor_name` |
| Status | `active` |
| Known present by | `7.4` |
| Locked snapshots | `9.0.23, 9.1.24, 9.2.24, 9.3.25, 9.4.26, 9.5.25, 9.6.24, 10.23, 11.22, 12.22, 13.23, 14.24, 15.19, 16.15, 17.11, 18.6, 19beta3` |
| Macros | `ERRCODE_INVALID_CURSOR_NAME, ERRCODE_UNDEFINED_CURSOR` |
| Aliases | `ERRCODE_UNDEFINED_CURSOR` |

<!-- source facts: data/errcodes/34000.json -->
<!-- END SQLSTATE FACTS -->

## Meaning {#meaning}

The executor's portal path reports `cursor "%s" does not exist` when `FETCH` cannot find the named portal. Cursor names are tied to a backend session, and ordinary cursors are tied to a transaction unless declared with an appropriate hold option. A pool checkout therefore must keep declaration and use on the same connection and within the intended transaction boundary.

## Diagnosis {#diagnosis}

Record SQLSTATE, the dynamic cursor name, backend PID, and transaction status. Check the application path for an earlier `CLOSE`, an implicit commit, a connection return to the pool, or a different session. `pg_cursors` and the server-side session identity can help when the diagnosis runs on the same backend; a new connection cannot inspect or fetch a cursor owned by the old one.

## Response {#response}

For an explicit block, rollback after the failed FETCH before issuing more commands. Re-declare the cursor on the same session, or deliberately redesign the workflow to materialize keys/results when work must cross pool checkouts. Use a holdable cursor only when its commit-survival semantics are intended; it does not make a cursor portable between sessions.

## Messages {#messages}

The selected core path uses `cursor "%s" does not exist`; protocol and PL/pgSQL paths can use `portal "%s"` or the same cursor wording. The name is dynamic, so use the SQLSTATE and diagnostic fields rather than a full-message equality check across versions and callers.

## Representative case {#case}

The shared registry in `verify/cases/34000/snippets.json` (SHA-256 `ad5b519e8ca30fd2636b1d0bace32dc18558cccc7b9d360081c27b12a1405f08`) declares and closes a cursor, proves the missing-FETCH error, rolls back, and recreates it in the same session. See [the public case export](../data/cases/34000.json) and [structured evidence](../data/evidence/34000.json).

<!-- BEGIN SQLSTATE SNIPPET: cursor_lifecycle_recovery -->

```sql
BEGIN;
DECLARE cursor_name CURSOR FOR SELECT 1;
CLOSE cursor_name;
FETCH cursor_name;
ROLLBACK;
BEGIN;
DECLARE cursor_name CURSOR FOR SELECT 1;
FETCH cursor_name;
CLOSE cursor_name;
COMMIT;
```
<!-- END SQLSTATE SNIPPET -->

The runner substitutes a unique cursor name. The first `FETCH` is intentionally after `CLOSE`; the second `FETCH` occurs after a new `BEGIN` and declaration, so the successful row proves lifecycle repair rather than a client-side mock.

The selected case observed `34000` on PostgreSQL 18.6 and 10.21 after closing a named cursor and attempting `FETCH`. The failed block reached `INERROR`; rollback followed by a new declaration, fetch, close, and commit returned the session to `IDLE`.

## Versions {#versions}

The locked catalogue records this condition across the formal snapshots. 18.6 and 10.23 source scans both contain the portal and PL/pgSQL paths; the selected runtime case observes the same SQLSTATE on 18.6 and 10.21, while exact source line and caller wording vary.

## Related {#related}

Compare [24000 invalid cursor state](../24000/) for a cursor that exists but is not positioned, and [26000 invalid prepared statement name](../26000/) for another session-local resource.

## Sources {#sources}

- [`src.errcodes.18.6`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/utils/errcodes.txt) (SHA-256 `6e8de346643ba84aa3c9c6a73360acfc7b2dfb89162c06c08ce9bf5bcd5bbcba`)
- [`src.portalcmds.18.6`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/commands/portalcmds.c#L197-L199) (SHA-256 `e71c5bdb2da67771fb5f42f18b823e8af3ee6c31d5314e97a47ad18f7788bf35`)
- [`DECLARE documentation`](https://www.postgresql.org/docs/18/sql-declare.html) · local call scan `src.calls.REL_18_6` (SHA-256 `9ee8a0e81d8f0825c5c1ae45583439859a26e602bdd4ce2f2a62aa278867ccbf`)
