# 24000 — invalid_cursor_state

> PostgreSQL SQLSTATE 24000: invalid_cursor_state for a cursor that is not positioned on a row.
---

# 24000 — invalid_cursor_state

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

`24000` means the cursor or portal state does not satisfy the operation. The selected `CURRENT OF` case declares a valid cursor but tries to update before FETCH has positioned it, producing “cursor ... is not positioned on a row”.

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

| Field | Value |
| --- | --- |
| SQLSTATE | `24000` |
| Condition | `invalid_cursor_state` |
| 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_STATE` |
| Aliases | `—` |

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

## Meaning {#meaning}

`WHERE CURRENT OF` needs a cursor declared for an updatable query, with a current row selected by FETCH. Declaration alone is not positioning. PostgreSQL uses `24000` for other cursor states too, including non-SELECT, held, non-updatable, or missing FOR UPDATE/SHARE references.

## Diagnosis {#diagnosis}

Record the cursor name and operation, then inspect declaration, transaction lifetime, FETCH direction, and whether the cursor is updatable. In an explicit transaction the selected error leaves `INERROR`; `ROLLBACK` restores `IDLE`. The selected repair updates by primary key, so it verifies transaction recovery and a safe direct repair, not a second `DECLARE`/`FETCH` path.

## Response {#response}

Position the cursor with FETCH before `CURRENT OF`, or use a deterministic key-based update when that is the intended operation. Keep cursor lifetime and transaction boundaries explicit, and roll back a failed transaction before issuing the repair.

## Observed diagnostics {#messages}

`18.6 (Homebrew) / latest`:SQLSTATE `24000`; primary `cursor "item_cursor" is not positioned on a row`; after_error `INERROR`; after_rollback `IDLE`.
`10.21 (Debian 10.21-1.pgdg90+1) / pg10`:SQLSTATE `24000`; primary `cursor "item_cursor" is not positioned on a row`; after_error `INERROR`; after_rollback `IDLE`.

## Representative case {#case}

The runner reads these statements from `verify/cases/24000/snippets.json` (SHA-256 `cafca57825378362df0527384614bf658bf86cd3101e1eb520ea0641819f2789`) and qualifies the temporary table names; the complete setup, assertions, and cleanup are in the [case export](../data/cases/24000.json).

<!-- BEGIN SQLSTATE SNIPPET: current_of_before_fetch -->
```sql
-- create
CREATE TABLE items(id integer PRIMARY KEY, note text NOT NULL);
-- seed
INSERT INTO items VALUES (1, 'seed');
-- begin
BEGIN;
-- declare
DECLARE item_cursor CURSOR FOR SELECT id FROM items FOR UPDATE;
-- trigger
UPDATE items SET note = 'bad' WHERE CURRENT OF item_cursor;
-- rollback
ROLLBACK;
-- repair
UPDATE items SET note = 'repaired' WHERE id = 1;
-- verify
SELECT id, note FROM items;
```
<!-- END SQLSTATE SNIPPET -->

The SQLSTATE, diagnostic, transaction-state, and repair assertions for this excerpt are produced from the shared registry; [structured evidence](../data/evidence/24000.json) · [case export](../data/cases/24000.json).

Authored evidence IDs: `identity`, `current-of`, `runtime`. Selected runtime records: `runtime.24000-batch1-latest2-20260909.latest`, `runtime.24000-batch1-pg10-20260909.pg10`.

## Versions {#versions}

The locked catalogue observes the condition by `7.4` and in all listed formal snapshots. The selected before-FETCH path passes on 18.6 and 10.21; other cursor-state templates require separate cases.

## Related {#related}

Compare [25001 active SQL transaction](../25001/), [25P02 failed SQL transaction](../25p02/), and [34000 invalid cursor name](../34000/).

## Sources {#sources}

- [`src.errcodes.18.6`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/utils/errcodes.txt) (SHA-256 `6e8de346643ba84aa3c9c6a73360acfc7b2dfb89162c06c08ce9bf5bcd5bbcba`)
- `src.calls.REL_18_6` (SHA-256 `9ee8a0e81d8f0825c5c1ae45583439859a26e602bdd4ce2f2a62aa278867ccbf`)
- [`src.execCurrent.18.6`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/executor/execCurrent.c#L135-L138) (SHA-256 `45c8e48b3a5dc46f1014482dc88e0a26c25c3a4c403372112474d9171253d8a9`)
- [`doc.declare.18.6`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/doc/src/sgml/ref/declare.sgml#L275-L285) (SHA-256 `f1d72befb9a989aa32123560aea784a8bccfcb3d3930804a101d8245e5a3cc95`) · [official documentation](https://www.postgresql.org/docs/18/sql-declare.html)
- [`doc.update.18.6`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/doc/src/sgml/ref/update.sgml#L203-L211) (SHA-256 `47dd724cd724bd77478215a4853aa1f985996a0e5bba9b2451a1ad1ebb6e51fc`) · [official documentation](https://www.postgresql.org/docs/18/sql-update.html)
