# 42P03 — Duplicate cursor

> PostgreSQL SQLSTATE 42P03: Duplicate cursor (duplicate_cursor), source-backed diagnosis and recovery guidance.
---

# 42P03 — Duplicate cursor

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

`42P03` (**duplicate_cursor**) reports a cursor/portal name collision, but the same SQLSTATE covers an ERROR, a replacement WARNING, and a PL/pgSQL-specific active-cursor ERROR. The caller and the `CreatePortal` options determine which behavior occurred.

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

| Field | Value |
| --- | --- |
| SQLSTATE | `42P03` |
| Condition | `duplicate_cursor` |
| 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_DUPLICATE_CURSOR` |
| Aliases | `—` |

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

## Meaning {#meaning}

`CreatePortal(name, allowDup, dupSilent)` raises ERROR `cursor "%s" already exists` when `allowDup` is false. When `allowDup` is true, it can emit WARNING `closing existing cursor "%s"` unless `dupSilent` is set, then drops the old portal and creates the new one. PL/pgSQL takes a different branch: `SPI_cursor_find` detects an already active named cursor and raises ERROR `cursor "%s" already in use`.

## Diagnosis {#diagnosis}

Record the exact name, source (SQL portal creation or PL/pgSQL `OPEN`), severity, and transaction state. For SQL, inspect portal creation/reuse and `pg_cursors` where that view exposes the portal; for PL/pgSQL, inspect the cursor variable and the `OPEN`/`CLOSE` flow. The `allowDup` replacement branch is not evidence that a user-level duplicate declaration always warns, and the PL/pgSQL branch is not the portal-manager replacement path.

## Response {#response}

For an ERROR from a non-replacing portal path, close the old portal or choose a name whose transaction lifetime is correct. A WARNING replacement path already closes the old portal, so the application must verify that replacement is intended rather than treating the warning as a failed close. For PL/pgSQL, close the named cursor before reopening it or use a distinct cursor variable/name. Only ERROR paths require explicit-transaction recovery with a savepoint rollback or transaction rollback; reconnecting can hide pool ownership problems.

## Messages {#messages}

Representative 18.6 messages are:

| Path | Severity | Primary |
| --- | --- | --- |
| `CreatePortal(..., allowDup=false, ...)` | `ERROR` | `cursor "%s" already exists` |
| `CreatePortal(..., allowDup=true, dupSilent=false)` | `WARNING` | `closing existing cursor "%s"` |
| PL/pgSQL named cursor already found | `ERROR` | `cursor "%s" already in use` |

The `%s` value is dynamic; no concrete cursor name is claimed.

## Versions {#versions}

From 7.4 through 18.6 and 19beta3; the catalogue range is not a precise introduction claim. The selected portal-manager and PL/pgSQL branches are fixed 18.6 source paths, and runtime was not run.

## Related {#related}

- [`34000`](../34000/)
- [`42P02`](../42p02/)
- [`25P02`](../25p02/)

## Sources {#sources}
- `src.errcodes.42P03.18.6` — `src/backend/utils/errcodes.txt` line 378, fixed at `724edf9bde9d356724ad384a2e196edc3c9f80f7`; SHA-256 `6e8de346643ba84aa3c9c6a73360acfc7b2dfb89162c06c08ce9bf5bcd5bbcba` ([source](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/utils/errcodes.txt#L378)).

- `src.call.42P03.800a8278cf453417f5af6d6e` — `src/backend/utils/mmgr/portalmem.c` lines 185-187, fixed at `724edf9bde9d356724ad384a2e196edc3c9f80f7`; SHA-256 `8e29a302a7837316765acc798f9ff6eb22a330e8098a02794cd4d683601d3b04` ([source](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/utils/mmgr/portalmem.c#L185-L187)).
- `src.call.42P03.208bdd3b576b5021fb35916d` — `src/backend/utils/mmgr/portalmem.c` lines 189-192, fixed at `724edf9bde9d356724ad384a2e196edc3c9f80f7`; SHA-256 `8e29a302a7837316765acc798f9ff6eb22a330e8098a02794cd4d683601d3b04` ([source](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/utils/mmgr/portalmem.c#L189-L192)).
- `src.call.42P03.06c0580b43488eece2de1973` — `src/pl/plpgsql/src/pl_exec.c` lines 2895-2897, fixed at `724edf9bde9d356724ad384a2e196edc3c9f80f7`; SHA-256 `0df3c70a6bccf6dddf443fb99e152a16a67009827c44a820597e98e585cb8b20` ([source](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/pl/plpgsql/src/pl_exec.c#L2895-L2897)).
- `src.calls.REL_18_6.42P03` — resolved core call groups; SHA-256 `9ee8a0e81d8f0825c5c1ae45583439859a26e602bdd4ce2f2a62aa278867ccbf`.
- [`authored evidence`](../data/evidence/42p03.json) — source claims, message roles, and runtime boundary.
