# Guides

> Shared methods for reading PostgreSQL diagnostics, recovering transaction state, and choosing client boundaries.
---

## Read the wire diagnostic {#read-diagnostics}

Start with the protocol message type. PostgreSQL sends an `ErrorResponse` for an error and a `NoticeResponse` for a notice; both carry identified fields terminated by a zero byte. The `C` field is the non-localized SQLSTATE, `M` is the primary message, and `D`/`H` are optional detail and hint. `S` can be localized; `V` is the non-localized severity when supplied. Schema (`s`), table (`t`), column (`c`), constraint (`n`), position (`P`), context (`W`), source file (`F`), line (`L`), and routine (`R`) are conditional fields: preserve them when present and do not infer missing fields. See the [PostgreSQL 18 error and notice fields](https://www.postgresql.org/docs/18/protocol-error-fields.html) and [message formats](https://www.postgresql.org/docs/18/protocol-message-formats.html).

A directory class is only a classifier. Read the actual protocol severity and message type before deciding whether a command failed. A `WARNING` or `NOTICE` may arrive as a notice while the command completes; `ERROR` arrives as an error and normally changes explicit transaction state. A successful `CommandComplete` has a command tag and no ErrorResponse SQLSTATE. Keep the server version and operation beside the fields because one SQLSTATE can have several source paths.

## Recover the right transaction boundary {#transaction-recovery}

Autocommit gives each statement its own transaction boundary. After a statement error, an otherwise usable connection can execute a later statement. In an explicit transaction, an unhandled `ERROR` leaves the transaction failed; PostgreSQL then rejects ordinary commands with `25P02` until rollback. `25P02` is a follow-on observation, not the first error. Use the [ROLLBACK](https://www.postgresql.org/docs/18/sql-rollback.html) and [SAVEPOINT](https://www.postgresql.org/docs/18/sql-savepoint.html) references for the boundary you actually established.

If an already-existing savepoint surrounds one optional unit, use `ROLLBACK TO SAVEPOINT name`, then continue in the outer transaction or release the savepoint. Work before that savepoint remains. Without such a savepoint, `ROLLBACK` ends the failed transaction and a retry starts a new one. The accepted [25P02 case](../data/cases/25p02.json) shows root `23505`, subsequent `25P02`, and recovery; [23505 evidence](../data/evidence/23505.json) records the narrower savepoint boundary.

A PL/pgSQL `EXCEPTION` block is another narrow boundary. Changes made inside the block are rolled back before its handler runs, while earlier outer work remains. In the handler, `SQLSTATE` and `SQLERRM` identify the current exception; `GET STACKED DIAGNOSTICS` can retrieve `RETURNED_SQLSTATE`, message, detail, hint, context, and object fields. See [error trapping](https://www.postgresql.org/docs/18/plpgsql-control-structures.html#PLPGSQL-ERROR-TRAPPING), [stacked diagnostics](https://www.postgresql.org/docs/18/plpgsql-control-structures.html#PLPGSQL-GET-DIAGNOSTICS), and [RAISE](https://www.postgresql.org/docs/18/plpgsql-errors-and-messages.html). Keep the handler around the smallest operation that can be classified safely; `WHEN OTHERS` should not discard the original fields.

## Retry a complete unit only when its outcome is known {#retry}

Retry is a business decision, not a property of a SQLSTATE class. For a transient serialization or deadlock error, retry the whole transaction from a fresh snapshot when the operation is safe. Do not replay only the last statement if earlier reads, writes, locks, notifications, or external calls formed one business unit. A transaction may have committed while the client timed out or lost its connection before receiving the result; completion can then be uncertain. Use an idempotency key, durable business key, or status query before repeating externally visible work.

For authorization failures, missing objects, and invalid data, first inspect and repair the permission, object, or input; then decide whether a corrected operation is safe to run. A warning that already completed the command should not trigger automatic replay. Preserve the first diagnostic and transaction state; log a later `25P02` as a consequence. Backoff and attempt limits control load, but cannot make a non-idempotent operation safe. A clear server-side timeout or cancellation is a failed operation; a lost connection before the client knows the outcome is a separate uncertain-completion case.

## Correlate application and server logs {#logging}

Record SQLSTATE, non-localized severity, primary message, detail, hint, object names, position/context, server version, backend PID when available, and client transaction state before formatting a human message. Add an application request or idempotency key and timestamp so the operation can be found in server logs. PID, session identifiers, and query ID are correlation aids, not replacements for protocol fields.

CSV and JSON server logs are structured but do not have the same shape as an ErrorResponse. PostgreSQL 18 documents `csvlog` columns including severity, SQLSTATE, message, detail, hint, context, query, source location, application name, backend type, and query ID. `jsonlog` is JSON and may omit null-valued fields; consumers should ignore future fields. Configure `log_destination` and `logging_collector` using the [logging reference](https://www.postgresql.org/docs/18/runtime-config-logging.html), then correlate by PID/session/request rather than assuming a CSV column or JSON key equals a protocol field.

## PL/pgSQL and custom conditions {#plpgsql}

`RAISE` can report `DEBUG`, `LOG`, `INFO`, `NOTICE`, `WARNING`, or `EXCEPTION`; `EXCEPTION` normally aborts the current transaction. It can select a condition name or five-character SQLSTATE and set `MESSAGE`, `DETAIL`, `HINT`, `SCHEMA`, `TABLE`, `COLUMN`, `DATATYPE`, or `CONSTRAINT` with `USING`. PostgreSQL permits any five-character code made of digits and upper-case ASCII letters except `00000`, including custom codes. A code ending in three zeroes is a category and can only be trapped by that category. See [RAISE syntax](https://www.postgresql.org/docs/18/plpgsql-errors-and-messages.html#PLPGSQL-ERRORS-AND-MESSAGES-RAISE).

A named handler matches its condition and documented aliases; a class handler is broader. `WHEN OTHERS` catches every error condition except `QUERY_CANCELED` and `ASSERT_FAILURE`; notice-level messages are not exceptions. A custom code raised by PL/pgSQL proves that deliberate report only, not natural emission by core, contrib, FDW, ECPG, or a driver.

## What clients expose {#clients}

The table pins each row to an inspected release or primary documentation. It does not claim that all six clients were runtime-tested here.

| Client | Inspected release/source | Exposed diagnostic | Boundary |
| --- | --- | --- | --- |
| libpq | PostgreSQL 18.6, commit [`724edf9b`](https://github.com/postgres/postgres/tree/724edf9bde9d356724ad384a2e196edc3c9f80f7) | [`PQresultErrorField`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/interfaces/libpq/libpq-fe.h#L588) reads fields from an error or warning `PGresult`; [`PQstatus`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/interfaces/libpq/libpq-fe.h#L407) describes connection state. | A failed startup may have no `PGresult`; result-field access is not a universal startup-error API. Keep connection text and server logs separate. |
| psycopg | 3.3.5, tag peeled to commit [`ea542c95`](https://github.com/psycopg/psycopg/tree/ea542c9534cc9841d50450e0c156b549cc6c805a) | [`Error`](https://github.com/psycopg/psycopg/blob/ea542c9534cc9841d50450e0c156b549cc6c805a/psycopg/psycopg/errors.py#L267-L302) exposes `sqlstate`, `diag`, `pgconn`, and `pgresult`; the same source builds named SQLSTATE exception classes. | Connection errors may have `sqlstate=None`; query errors can carry result diagnostics. |
| PostgreSQL JDBC | pgjdbc `REL42.7.8`, commit [`9a5492d9`](https://github.com/pgjdbc/pgjdbc/tree/9a5492d99ce43507e318ce4bb56030f74b773d48) | [`PSQLException.java`](https://github.com/pgjdbc/pgjdbc/blob/9a5492d99ce43507e318ce4bb56030f74b773d48/pgjdbc/src/main/java/org/postgresql/util/PSQLException.java#L30-L38) maps `ServerErrorMessage.getSQLState()` to JDBC `getSQLState()` and retains the server message. | Client and transport exceptions are also wrapped; type and server-message presence matter. |
| pgx/pgconn | pgx `v5.7.6`, commit [`a2fca037`](https://github.com/jackc/pgx/tree/a2fca037434a0a7096b095d4ed87cdffb03b626e) | [`errors.go`](https://github.com/jackc/pgx/blob/a2fca037434a0a7096b095d4ed87cdffb03b626e/pgconn/errors.go#L48-L60) defines `PgError.SQLState()` for server errors. | Connection, context, and parse errors are other Go error types or wrappers; use `errors.As`. |
| node-postgres / pg-protocol | node-postgres `pg@8.16.3`, commit [`8f8e7315`](https://github.com/brianc/node-postgres/tree/8f8e7315e8f7c1bb01e98fdb41c8c92585510782) | [`messages.ts`](https://github.com/brianc/node-postgres/blob/8f8e7315e8f7c1bb01e98fdb41c8c92585510782/packages/pg-protocol/src/messages.ts#L77-L115) and [`parser.ts`](https://github.com/brianc/node-postgres/blob/8f8e7315e8f7c1bb01e98fdb41c8c92585510782/packages/pg-protocol/src/parser.ts#L357-L386) parse `E`/`N` fields into database and notice messages. | `code`, severity, detail, hint, and object fields are server diagnostics; socket errors are separate JavaScript errors. |
| Npgsql | Npgsql `v10.0.0`, commit [`a1802184`](https://github.com/npgsql/npgsql/tree/a18021849f244716d3b68eefd705677f131f9ace) | [`PostgresException.cs`](https://github.com/npgsql/npgsql/blob/a18021849f244716d3b68eefd705677f131f9ace/src/Npgsql/PostgresException.cs#L44-L75) exposes `SqlState`; the [diagnostics guide](https://www.npgsql.org/doc/diagnostics/exceptions_notices.html) distinguishes notices and exception types. | `NpgsqlException` can wrap network/client failures; notices arrive through the `Notice` event, not as command errors. |

Named exception classes, constants, and wrappers are convenience mappings. Cross-client evidence remains the protocol SQLSTATE and fields; a client-side message or null SQLSTATE does not overwrite a confirmed server-log code. Existing psycopg 3.3.5/libpq 18.6 startup records with driver-null and server `28P01`/`53300` demonstrate this boundary for that stack only.

---

Backlinks:

- [SQLSTATE Atlas](/)
