error-handling
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.
Install this skill
Choose your coding agent and copy a project or personal installation command.
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/error-handling -a codex -ynpx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/error-handling -a codex -g -ynpx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/error-handling -a claude-code -ynpx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/error-handling -a claude-code -g -ynpx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/error-handling -a github-copilot -ynpx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/error-handling -a github-copilot -g -ynpx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/error-handling -a cursor -ynpx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/error-handling -a cursor -g -ynpx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/error-handling -a gemini-cli -ynpx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/error-handling -a gemini-cli -g -ygemini skills install https://github.com/matejformanek/postgres-claude.git --scope workspace --path .claude/skills/error-handlingSkill instructions
View source on GitHub ↗# Error handling — actionable rules
Reference doc: `knowledge/idioms/error-handling.md` (read it for the *why*).
## When you write a new error report
1. **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.
2. **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)`.
3. **`errmsg` style — no leading capital, no trailing period, no newline,
one phrase.** Example: `errmsg("relation \"%s\" does not exist", name)`.
4. **`errdetail` / `errhint` / `errcontext` are full sentences** — capital,
period, may be multi-sentence. `errhint` should be actionable.
5. **Quote identifiers as `\"%s\"`.** Don't quote SQL keywords or numbers.
6. **Don't put SQLSTATE or severity in the message text** — `errcode()` and
the logger handle them.
7. **Don't concatenate fragments into the format string** — breaks gettext.
Build via printf args.
8. **Use `errmsg_internal` for messages that should not be translated**
(developer-only "can't happen" cases). `elog` already does this.
9. **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.
```c
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
- [ ] `errcode()` set explicitly, not relying on default INTERNAL_ERROR.
- [ ] `errmsg` is a string literal (for gettext extraction), lowercase start,
no period.
- [ ] `errdetail` / `errhint` are complete sentences.
- [ ] Identifiers quoted as `\"%s\"`.
- [ ] No `goto cleanup` after `ereport(ERROR, ...)`.
- [ ] If using `PG_TRY`: catch block rethrows or aborts; modified locals are
`volatile`.
- [ ] For "should never happen" use `elog`, not `ereport` with hand-written
`errmsg_internal`.
- [ ] No newlines in `errmsg`.
- [ ] No fragment concatenation.
## 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.