Incident brief
Cursor does not exist
A CLOSE (or FETCH/MOVE) named a cursor that was never declared, or was already closed. PostgreSQL has no OPEN statement — a cursor exists only once you DECLARE it.
In 10 seconds
- What
- Cursor does not exist
- What triggers it
- Begin a transaction.
- The fix
- DECLARE the cursor before you FETCH, MOVE, or CLOSE it — there is no implicit OPEN.
- Proof
- Reproduced on PostgreSQL 16.14 → CLOSE of an undeclared cursor was rejected with SQLSTATE 34000, aborting the transaction. Declaring the cursor first let FETCH and CLOSE succeed.
The fix
What to do right now
The immediate, application-level response to this error.
- DECLARE the cursor before you FETCH, MOVE, or CLOSE it — there is no implicit OPEN.
- Remember non-holdable cursors are implicitly closed at COMMIT/ROLLBACK; don't reuse the name across transactions.
- Query pg_cursors to see which cursors are actually open in the current session.
-- declare the cursor before you FETCH or CLOSE it
BEGIN;
DECLARE order_cursor CURSOR FOR SELECT g FROM generate_series(1, 3) AS g;
FETCH ALL FROM order_cursor;
CLOSE order_cursor;
COMMIT;Diagnose
See it live on the server
Run these against the affected instance to confirm the diagnosis before you act.
Standard triage — not specific to this error
These are canonical PostgreSQL system-catalog queries, shown as SQL to run. No sample output is attached because this is general triage, not a captured lab transcript.This SQLSTATE does not have an error-specific live snapshot yet. These are the canonical system-catalog queries you run against the affected server to see the problem in real time — standard triage, not a reproduced transcript.
What is running right now
Active backends, how long each has been running, and what it is waiting on.
SELECT pid,
state,
wait_event_type,
wait_event,
now() - query_start AS running_for,
left(query, 80) AS query
FROM pg_stat_activity
WHERE state <> 'idle'
AND pid <> pg_backend_pid()
ORDER BY running_for DESC NULLS LAST;Who is blocking whom
Turn raw blocking PIDs into the actual queries on both sides of the wait.
SELECT blocked.pid AS blocked_pid,
blocked.query AS blocked_query,
blocking.pid AS blocking_pid,
blocking.query AS blocking_query
FROM pg_stat_activity AS blocked
JOIN LATERAL unnest(pg_blocking_pids(blocked.pid)) AS b(pid) ON true
JOIN pg_stat_activity AS blocking ON blocking.pid = b.pid
WHERE cardinality(pg_blocking_pids(blocked.pid)) > 0;Locks that are still waiting
Every lock a backend has requested but not yet been granted.
SELECT l.pid,
l.locktype,
l.mode,
l.granted,
COALESCE(c.relname, l.transactionid::text) AS object
FROM pg_locks l
LEFT JOIN pg_class c ON c.oid = l.relation
WHERE NOT l.granted
ORDER BY l.pid;Why it happens
What PostgreSQL is telling you
The mechanism behind the error, grounded in the official manual — not paraphrased.
PostgreSQL 16 Documentation — SQL Commands: CLOSE
PostgreSQL does not have an explicit OPEN cursor statement; a cursor is considered open when it is declared. Use the DECLARE statement to declare a cursor.Read the full section on postgresql.org →
Closing a cursor that was never declared
There is no cursor named order_cursor in this session — nothing declared it — so CLOSE raises SQLSTATE 34000, and the transaction switches to the aborted state.Recovering the aborted transaction
The transaction was aborted by the error, so it must be rolled back before the session accepts new work; after ROLLBACK the next statement runs normally.Reproduce & verify
A real, single-session PostgreSQL reproduction
A literal transcript of SQL run against a live PostgreSQL instance in an isolated lab — the commands below are exactly what was executed.
- 1Begin a transaction.
- 2CLOSE a cursor name that was never declared.
- 3PostgreSQL raises SQLSTATE 34000 and the transaction is now in the aborted state.
One client: inside a transaction, CLOSE names a cursor that was never declared.
-- The cursor error is self-contained inside one transaction; no tables required.
SELECT 'no schema needed' AS setup_note;BEGIN;
CLOSE order_cursor;ROLLBACK;
SELECT 'ok' AS session_after_rollback;What PostgreSQL actually returned
setup_note
------------------
no schema needed
(1 row)BEGIN
ERROR: cursor "order_cursor" does not existROLLBACK
session_after_rollback
------------------------
ok
(1 row)The full DECLARE → FETCH → CLOSE sequence was run inside one transaction and returned the rows and clean command tags, proving the cursor only needed to be declared first.
Declare the cursor, fetch all rows, then close it — all in one transaction.
Without this
Above: CLOSE without a DECLARE raises SQLSTATE 34000.
With this, tested
Below: DECLARE first, and FETCH/CLOSE both succeed.
What Pro unlocks here
- The exact prevention SQL — copy-paste ready
- Raw psql output captured from the Docker lab
- A senior-DBA action list to take it further
- Live monitoring queries to catch it in production
- The deeper audit: fix-that-fails counterexample, GUC before/after, server-log evidence
Related & next steps
Follow the thread
Everything this error touches — jump straight to the sibling error, term, runbook, or parameter.
Verification
- Last verified
- 2026-07-19 (isolated lab, PostgreSQL 16.14)
- Reviewed by
- Verified against PostgreSQL 16.14 in an isolated lab environment
- Audit status
- reviewed