Error handling — actionable rules
Reference doc: knowledge/idioms/error-handling.md (read it for the why).
When you write a new error report
- Pick
ereport for user-visible errors, elog for "should never happen".
elog(ERROR, "cache lookup failed for relation %u", oid) is the canonical
internal-error idiom. ereport(ERROR, errcode(...), errmsg(...)) is for
anything the user can trigger.
- Always pass an
errcode() in ereport. Default is ERRCODE_INTERNAL_ERROR,
which is rarely what you want. Pick the most specific SQLSTATE from
src/backend/utils/errcodes.txt. For file/socket errors use
errcode_for_file_access() / errcode_for_socket_access() right after the
syscall (they consume errno). Pair with %m in the errmsg format
string to splice strerror(errno) into the message, e.g.
errmsg("could not open file \"%s\": %m", path).
errmsg style — no leading capital, no trailing period, no newline,
one phrase. Example: errmsg("relation \"%s\" does not exist", name).
errdetail / errhint / errcontext are full sentences — capital,
period, may be multi-sentence. errhint should be actionable.
- Quote identifiers as
\"%s\". Don't quote SQL keywords or numbers.
- Don't put SQLSTATE or severity in the message text —
errcode() and
the logger handle them.
- Don't concatenate fragments into the format string — breaks gettext.
Build via printf args.
- Use
errmsg_internal for messages that should not be translated
(developer-only "can't happen" cases). elog already does this.
- Don't clobber
errno between the failing syscall and the ereport.
Any palloc, syscall, or function call may overwrite it. Capture into a
local (int save_errno = errno;) if you need to do work first, or
restore via errno = save_errno; before the ereport.
Picking elevel
DEBUG1..DEBUG5 — verbose tracing, gated by log_min_messages.
LOG — operational events. Goes to server log, not to client by default.
INFO — explicit user-requested output (e.g. VACUUM VERBOSE).
NOTICE — expected events the user should know about.
WARNING — unexpected non-fatal. Distinct from NOTICE.
ERROR — abort current transaction, longjmp out. Most common choice.
FATAL — terminate this backend process. Used for auth failure, fatal
startup errors.
PANIC — only if continuing would corrupt shared state (xlog write failure,
shared memory invariant broken). Postmaster restarts the cluster.
Default to ERROR. Use FATAL/PANIC only with strong justification.
Critical: ereport(ERROR) does not return
It longjmps to the nearest PG_TRY or to PostgresMain. You do not write
cleanup code after it. Anything reached "after" an ERROR in the source is
dead code from a runtime perspective. Don't goto cleanup; let transaction
abort handle memory contexts, locks, buffer pins, etc.
For fds specifically, open via OpenTransientFile() (registers with the
transaction's ResourceOwner so it closes on abort) rather than raw open(2).
PG_TRY / PG_CATCH — when to use
Default answer: don't. Almost all backend code lets ERROR propagate.
Use PG_TRY only when:
- You hold a resource that won't be released by transaction abort (e.g. a
Python interpreter handle in PL/Python, a libxml2 parser).
- You implement a language that must convert backend ERROR into a host
exception (PL/pgSQL, SPI re-entry).
- You're at a top-level loop (PostgresMain, bgworker main) and need to
resume after error.
When you do use it:
PG_CATCH must PG_RE_THROW() or call AbortCurrentTransaction() /
RollbackAndReleaseCurrentSubTransaction(). Never swallow silently.
- Locals modified in TRY and read in CATCH must be
volatile.
- Keep CATCH minimal — errors inside CATCH recurse on a 5-frame stack
(
ERRORDATA_STACK_SIZE in src/backend/utils/error/elog.c:154) before
PANIC.
- Prefer
PG_FINALLY over PG_CATCH whenever the cleanup is the same
on success and error (the common case). PG_FINALLY auto-rethrows; you
can't accidentally swallow the original error. Use PG_CATCH only when
the error path genuinely needs different work (e.g. converting to a
host-language exception).
FATAL is not caught by PG_TRY. Use PG_ENSURE_ERROR_CLEANUP
(storage/ipc.h) for FATAL-safe cleanup of process-external resources.
Adding "while doing X" context
Push an ErrorContextCallback rather than concatenating the context into the
message. Pop it on normal exit; PG_TRY auto-restores it on error.
ErrorContextCallback cb = { .callback = my_cb, .arg = state,
.previous = error_context_stack };
error_context_stack = &cb;
/* work */
error_context_stack = cb.previous;
The callback calls errcontext("processing row %d of \"%s\"", ...).
Soft errors
For input-parsing functions that accept an ErrorSaveContext *escontext:
use errsave(escontext, errcode(...), errmsg(...)) and check the node
afterwards. If escontext is NULL, behaves identically to ereport(ERROR, ...).
ereturn(escontext, dummy_value, ...) is the shorter form when you have no
post-report cleanup.
Checklist before committing
When in doubt, cite
Reference live examples by grepping similar paths:
src/backend/commands/*.c for user-facing DDL errors.
src/backend/access/heap/heapam.c for access-method internal errors.
src/backend/utils/cache/lsyscache.c for elog(ERROR, "cache lookup failed ...").
src/pl/plpgsql/src/pl_exec.c for non-trivial PG_TRY/PG_CATCH.
Cross-references
.claude/skills/memory-contexts/SKILL.md — AbortTransaction releases per-query contexts after ereport(ERROR); PG_TRY + volatile discipline.
.claude/skills/coding-style/SKILL.md — error-message style guide (lowercase errmsg, complete-sentence errdetail/errhint).
.claude/skills/debugging/SKILL.md — errfinish breakpoint to trap any ereport/elog; \errverbose from psql.
.claude/skills/locking/SKILL.md — spinlock + error-safety: spinlocks NOT released on error; LWLocks ARE.
.claude/skills/wal-and-xlog/SKILL.md — redo functions must ereport(PANIC), never ereport(ERROR) (no rollback during replay).
knowledge/idioms/error-handling.md — long-form idiom doc.
source/src/include/utils/errcodes.txt — canonical SQLSTATE list.
1---2name: error-handling3description: Write or review a PostgreSQL backend ereport / elog call — covers ereport vs elog, picking a SQLSTATE from errcodes.txt, errcode_for_file_access, errmsg / errdetail / errhint capitalisation rules, soft errors via escontext, PG_TRY / PG_CATCH longjmp-safe cleanup blocks, and the DEBUG / LOG / NOTICE / WARNING / ERROR / FATAL / PANIC elevel ladder. Use whenever a PG patch adds, edits, or reviews C in source/src/backend or contrib/ that reports an error or logs a message — picking elevel, choosing a SQLSTATE, formatting errmsg, wiring PG_TRY/PG_CATCH around a longjmp-unsafe block, or migrating a call to soft-error style. Skip for Python try/except, Go error returns, Rust Result / anyhow / thiserror, C++ exceptions, Java checked exceptions, Sentry / pino / Winston / log4j application logging, Oracle ORA-* / MySQL error codes, and general error-handling philosophy questions.4---56# Error handling — actionable rules78Reference doc: `knowledge/idioms/error-handling.md` (read it for the *why*).910## When you write a new error report11121. **Pick `ereport` for user-visible errors, `elog` for "should never happen".**13 `elog(ERROR, "cache lookup failed for relation %u", oid)` is the canonical14 internal-error idiom. `ereport(ERROR, errcode(...), errmsg(...))` is for15 anything the user can trigger.162. **Always pass an `errcode()`** in `ereport`. Default is `ERRCODE_INTERNAL_ERROR`,17 which is rarely what you want. Pick the most specific SQLSTATE from18 `src/backend/utils/errcodes.txt`. For file/socket errors use19 `errcode_for_file_access()` / `errcode_for_socket_access()` right after the20 syscall (they consume `errno`). Pair with `%m` in the `errmsg` format21 string to splice `strerror(errno)` into the message, e.g.22 `errmsg("could not open file \"%s\": %m", path)`.233. **`errmsg` style — no leading capital, no trailing period, no newline,24 one phrase.** Example: `errmsg("relation \"%s\" does not exist", name)`.254. **`errdetail` / `errhint` / `errcontext` are full sentences** — capital,26 period, may be multi-sentence. `errhint` should be actionable.275. **Quote identifiers as `\"%s\"`.** Don't quote SQL keywords or numbers.286. **Don't put SQLSTATE or severity in the message text** — `errcode()` and29 the logger handle them.307. **Don't concatenate fragments into the format string** — breaks gettext.31 Build via printf args.328. **Use `errmsg_internal` for messages that should not be translated**33 (developer-only "can't happen" cases). `elog` already does this.349. **Don't clobber `errno` between the failing syscall and the `ereport`.**35 Any palloc, syscall, or function call may overwrite it. Capture into a36 local (`int save_errno = errno;`) if you need to do work first, or37 restore via `errno = save_errno;` before the `ereport`.3839## Picking elevel4041- `DEBUG1..DEBUG5` — verbose tracing, gated by `log_min_messages`.42- `LOG` — operational events. Goes to server log, not to client by default.43- `INFO` — explicit user-requested output (e.g. VACUUM VERBOSE).44- `NOTICE` — expected events the user should know about.45- `WARNING` — unexpected non-fatal. Distinct from NOTICE.46- `ERROR` — abort current transaction, longjmp out. Most common choice.47- `FATAL` — terminate this backend process. Used for auth failure, fatal48 startup errors.49- `PANIC` — only if continuing would corrupt shared state (xlog write failure,50 shared memory invariant broken). Postmaster restarts the cluster.5152Default to `ERROR`. Use `FATAL`/`PANIC` only with strong justification.5354## Critical: `ereport(ERROR)` does not return5556It longjmps to the nearest `PG_TRY` or to PostgresMain. **You do not write57cleanup code after it.** Anything reached "after" an ERROR in the source is58dead code from a runtime perspective. Don't `goto cleanup`; let transaction59abort handle memory contexts, locks, buffer pins, etc.6061For fds specifically, open via `OpenTransientFile()` (registers with the62transaction's ResourceOwner so it closes on abort) rather than raw `open(2)`.6364## PG_TRY / PG_CATCH — when to use6566Default answer: **don't**. Almost all backend code lets ERROR propagate.6768Use `PG_TRY` only when:69- You hold a resource that won't be released by transaction abort (e.g. a70 Python interpreter handle in PL/Python, a libxml2 parser).71- You implement a language that must convert backend ERROR into a host72 exception (PL/pgSQL, SPI re-entry).73- You're at a top-level loop (PostgresMain, bgworker main) and need to74 resume after error.7576When you do use it:77- `PG_CATCH` must `PG_RE_THROW()` or call `AbortCurrentTransaction()` /78 `RollbackAndReleaseCurrentSubTransaction()`. Never swallow silently.79- Locals modified in TRY and read in CATCH must be `volatile`.80- Keep CATCH minimal — errors inside CATCH recurse on a 5-frame stack81 (`ERRORDATA_STACK_SIZE` in `src/backend/utils/error/elog.c:154`) before82 PANIC.83- **Prefer `PG_FINALLY` over `PG_CATCH`** whenever the cleanup is the same84 on success and error (the common case). `PG_FINALLY` auto-rethrows; you85 can't accidentally swallow the original error. Use `PG_CATCH` only when86 the error path genuinely needs different work (e.g. converting to a87 host-language exception).88- `FATAL` is not caught by PG_TRY. Use `PG_ENSURE_ERROR_CLEANUP`89 (`storage/ipc.h`) for FATAL-safe cleanup of process-external resources.9091## Adding "while doing X" context9293Push an `ErrorContextCallback` rather than concatenating the context into the94message. Pop it on normal exit; PG_TRY auto-restores it on error.9596```c97ErrorContextCallback cb = { .callback = my_cb, .arg = state,98 .previous = error_context_stack };99error_context_stack = &cb;100/* work */101error_context_stack = cb.previous;102```103104The callback calls `errcontext("processing row %d of \"%s\"", ...)`.105106## Soft errors107108For input-parsing functions that accept an `ErrorSaveContext *escontext`:109use `errsave(escontext, errcode(...), errmsg(...))` and check the node110afterwards. If `escontext` is NULL, behaves identically to `ereport(ERROR, ...)`.111`ereturn(escontext, dummy_value, ...)` is the shorter form when you have no112post-report cleanup.113114## Checklist before committing115116- [ ] `errcode()` set explicitly, not relying on default INTERNAL_ERROR.117- [ ] `errmsg` is a string literal (for gettext extraction), lowercase start,118 no period.119- [ ] `errdetail` / `errhint` are complete sentences.120- [ ] Identifiers quoted as `\"%s\"`.121- [ ] No `goto cleanup` after `ereport(ERROR, ...)`.122- [ ] If using `PG_TRY`: catch block rethrows or aborts; modified locals are123 `volatile`.124- [ ] For "should never happen" use `elog`, not `ereport` with hand-written125 `errmsg_internal`.126- [ ] No newlines in `errmsg`.127- [ ] No fragment concatenation.128129## When in doubt, cite130131Reference live examples by grepping similar paths:132- `src/backend/commands/*.c` for user-facing DDL errors.133- `src/backend/access/heap/heapam.c` for access-method internal errors.134- `src/backend/utils/cache/lsyscache.c` for `elog(ERROR, "cache lookup failed ...")`.135- `src/pl/plpgsql/src/pl_exec.c` for non-trivial `PG_TRY`/`PG_CATCH`.136137## Cross-references138139- `.claude/skills/memory-contexts/SKILL.md` — `AbortTransaction` releases per-query contexts after `ereport(ERROR)`; `PG_TRY` + `volatile` discipline.140- `.claude/skills/coding-style/SKILL.md` — error-message style guide (lowercase `errmsg`, complete-sentence `errdetail`/`errhint`).141- `.claude/skills/debugging/SKILL.md` — `errfinish` breakpoint to trap any `ereport`/`elog`; `\errverbose` from psql.142- `.claude/skills/locking/SKILL.md` — spinlock + error-safety: spinlocks NOT released on error; LWLocks ARE.143- `.claude/skills/wal-and-xlog/SKILL.md` — redo functions must `ereport(PANIC)`, never `ereport(ERROR)` (no rollback during replay).144- `knowledge/idioms/error-handling.md` — long-form idiom doc.145- `source/src/include/utils/errcodes.txt` — canonical SQLSTATE list.