Skip to content

PostgreSQL Error Handling — ereport/elog, PG_TRY/sigsetjmp, and the ErrorContext Escape Hatch

Contents:

Every long-lived server process must answer one question about errors: when something goes wrong mid-request, how does control reach a recovery point without leaking resources? The answer sits at the intersection of three distinct problems in systems design.

Structured control transfer. C has no native exceptions. The POSIX escape hatch is setjmp/longjmp — a pair that saves the CPU register state (program counter, stack pointer, callee-saved registers) at a guarded site and later restores it from an error site, bypassing all intermediate stack frames. sigsetjmp/siglongjmp extends the pair to also save and restore the signal mask, which matters for a server that handles SIGINT and SIGTERM. The critical property of this mechanism is that it performs a non-local return: the call stack between the setjmp site and the longjmp site is unwound without executing any C destructors, so the server must arrange resource cleanup through other means — most importantly, the memory-context delete cascades described in postgres-memory-contexts.md.

Diagnostic richness. A database error is not just a return code. The SQL standard (ISO 9075) defines a structured condition with at least: an SQLSTATE code (five-character alphanumeric class + subclass), a primary message, an optional detail, an optional hint, and a cursor position. Systems that use bare errno or integer error codes cannot carry this payload. PostgreSQL’s ErrorData struct captures all of it, plus source location, context stack, and server-log-only detail.

Severity hierarchy. Not all errors are created equal. A malformed user query deserves a message to the client followed by transaction abort; a backend running out of memory mid-query deserves the same; a corrupt shared-memory structure deserves to take down the whole cluster. The design space separates into a severity axis (DEBUG → LOG → INFO → NOTICE → WARNING → ERROR → FATAL → PANIC) and a destination axis (client only, server log only, both, neither). The combination determines not just what text goes where but what control flow happens afterward: return to caller, transaction abort via longjmp, process exit, or abort(2).

These three problems are why database engines do not just call perror(). They need a mechanism that is:

  1. Non-local (escapes arbitrary call depth without C exception support).
  2. Structured (carries SQLSTATE, message fields, source location).
  3. Severity-aware (decides control flow, not just output).

PostgreSQL’s error subsystem (src/backend/utils/error/elog.c, src/include/utils/elog.h) is the implementation of this design. It dates to the Berkeley POSTGRES era and has grown to ~3,800 lines of production-hardened C.

This section names the engineering patterns that appear across most production-grade database engines before reaching PostgreSQL’s specific choices.

The most common shape is a begin call that allocates state (the “error frame”) and a finish call that dispatches and recovers. The two-phase split exists because the error payload — message text, SQLSTATE code, detail, hint — must be assembled before any dispatch decision is made, and assembly can itself trigger nested log messages. A single-call report(level, msg) API cannot accommodate that; a begin/finish bracket can, because intermediate calls add fields to the current top-of-stack frame.

When a WARNING-level message fires inside the assembly of an ERROR-level message, the engine must handle both without corruption. The standard answer is a small stack of error frames (typically 3–8 slots). Each begin call claims the next slot; each finish call releases it. Shallow depth is a deliberate constraint: a stack overflow during error processing is a sign of infinite recursion, and the right answer is abort(), not a deeper stack.

Error handling must work even when the rest of the memory subsystem is exhausted. Engines pre-allocate a reserved memory region (often 8–32 KB) that is only used for error-message formatting. An out-of-memory condition can always produce its own diagnostic because this region is never given to normal allocations. A reset of this region at the start of recursive-error processing reclaims the space for the new message.

For recoverable errors (SQL-level ERROR: abort the transaction but keep the backend alive), the standard pattern is a guarded execution site using setjmp/sigsetjmp. The error path calls longjmp to that site. Multiple handlers nest by saving and restoring the handler pointer on a per-thread (or per-process) global. Each PG_TRY-equivalent saves the current handler, installs a new one, and restores the old one on both normal and error exit.

Context callback stack for call-site narration

Section titled “Context callback stack for call-site narration”

A bare longjmp delivers no call-stack information because C has no intrinsic stack unwinding. The conventional substitute is a context callback stack: a linked list of (callback, arg) pairs that are pushed by callers who want to annotate an error with their current state (“while parsing function X”, “while processing column Y”). errfinish (or its equivalent) walks the list before dispatching, appending each callback’s output to a “context” field that the client sees as the CONTEXT: line in the error message.

FATAL and PANIC as separate severity tiers

Section titled “FATAL and PANIC as separate severity tiers”

ERROR is recoverable within the session; FATAL terminates the session; PANIC terminates the whole cluster. These are not just “bigger errors” — they trigger different cleanup paths. FATAL calls proc_exit(), which runs on_proc_exit callbacks and exits the process cleanly. PANIC calls abort(), which generates a core dump and lets the postmaster observe an abnormal exit status before killing other backends. Engines that collapse these into a single “unrecoverable error” cannot provide the graduated response that cluster management requires.

Theory / conventionPostgreSQL name
Begin call (allocate error frame)errstart() / errstart_cold()
Finish call (dispatch + recover)errfinish()
Error frameErrorData struct
Error frame stackerrordata[ERRORDATA_STACK_SIZE] (depth 5)
Reserved error memoryErrorContext (MemoryContext)
setjmp handler pointerPG_exception_stack (sigjmp_buf *)
Guarded execution sitePG_TRY() macro (sigsetjmp)
Non-local transfer to handlerPG_RE_THROW()pg_re_throw()siglongjmp
Context callback stackerror_context_stack (ErrorContextCallback *)
Structured error payloadErrorData fields: sqlerrcode, message, detail, hint, context, …
SQLSTATE code30-bit packed encoding via MAKE_SQLSTATE
FATAL (session exit)proc_exit(1) path in errfinish
PANIC (cluster abort)abort() path in errfinish
Soft/non-longjmp error patherrsave() / ereturn() + ErrorSaveContext
Assertion failure (bypasses elog)Assert()ExceptionalCondition()abort()

PostgreSQL defines ten severity levels as integer constants in elog.h. In ascending order:

// severity levels — src/include/utils/elog.h
#define DEBUG5 10
#define DEBUG4 11
#define DEBUG3 12
#define DEBUG2 13
#define DEBUG1 14
#define LOG 15 /* server log only by default */
#define LOG_SERVER_ONLY 16
#define INFO 17 /* always to client regardless of client_min_messages */
#define NOTICE 18
#define WARNING 19
#define WARNING_CLIENT_ONLY 20
#define ERROR 21 /* abort transaction; longjmp to PG_TRY handler */
#define FATAL 22 /* abort process via proc_exit(1) */
#define PANIC 23 /* abort cluster via abort(2) */

The division at ERROR is load-bearing. Levels below ERROR never invoke longjmp; levels at or above ERROR never return to the call site (for ERROR the return is via longjmp; for FATAL/PANIC the process terminates).

LOG is special: it sorts between ERROR and FATAL for the purpose of should_output_to_server() (the is_log_level_output helper places LOG above ERROR in the log-destination comparison) but below ERROR for client-visible severity. This makes LOG a server-only informational channel that does not abort the transaction.

Each in-flight error occupies one slot in the errordata[] array:

// ErrorData — src/include/utils/elog.h
typedef struct ErrorData
{
int elevel; /* severity level */
bool output_to_server;
bool output_to_client;
bool hide_stmt;
bool hide_ctx;
const char *filename; /* __FILE__ of ereport() call */
int lineno;
const char *funcname;
const char *domain; /* gettext message domain */
const char *context_domain;
int sqlerrcode; /* SQLSTATE packed by MAKE_SQLSTATE */
char *message; /* primary message (translated) */
char *detail;
char *detail_log; /* server-log-only detail */
char *hint;
char *context; /* accumulated from callbacks */
char *backtrace;
const char *message_id; /* original (untranslated) string */
char *schema_name;
char *table_name;
char *column_name;
char *datatype_name;
char *constraint_name;
int cursorpos;
int internalpos;
char *internalquery;
int saved_errno;
struct MemoryContextData *assoc_context;
} ErrorData;

All char * fields are palloc’d into assoc_context, which is ErrorContext for normal errors. The message_id field preserves the original untranslated string for the emit_log_hook (extension authors may use it as a stable message identifier). saved_errno is captured at frame allocation time — before any argument evaluation can overwrite errno — so %m in format strings reliably expands to the right OS error text.

The stack has exactly five slots (ERRORDATA_STACK_SIZE = 5, elog.c:144). Overflow triggers ereport(PANIC, ...) after resetting the stack depth to −1 to free one slot, which is the self-bootstrapping escape hatch for “error during error processing.”

The user-facing entry points are macros, not functions:

// ereport / elog — src/include/utils/elog.h
#define ereport(elevel, ...) \
ereport_domain(elevel, TEXTDOMAIN, __VA_ARGS__)
#define ereport_domain(elevel, domain, ...) \
do { \
const int elevel_ = (elevel); \
pg_prevent_errno_in_scope(); \
if (errstart(elevel_, domain)) \
__VA_ARGS__, errfinish(__FILE__, __LINE__, __func__); \
if (elevel_ >= ERROR) \
pg_unreachable(); \
} while(0)
#define elog(elevel, ...) \
ereport(elevel, errmsg_internal(__VA_ARGS__))

The short-circuit if (errstart(...)) is the key optimization: for DEBUG messages below log_min_messages and client_min_messages, errstart returns false and the entire message-formatting apparatus is skipped. The pg_prevent_errno_in_scope() wrapper saves errno to a local before the argument expressions are evaluated, preventing any side-effecting sub-expression from clobbering it before errstart captures it into saved_errno. The pg_unreachable() after the block is a compiler hint that control never falls through after ereport(ERROR, ...).

A typical call site looks like:

// typical ereport — src/backend/storage/buffer/bufmgr.c (illustrative)
ereport(ERROR,
(errcode(ERRCODE_INTERNAL_ERROR),
errmsg("invalid page in block %u of relation %s",
blockNum, relpath)));

The comma-separated field calls (errcode, errmsg, errdetail, errhint, …) all return int (0), so the comma operator evaluates them left-to-right, building up the current ErrorData frame, before errfinish is called.

errstart: frame allocation and level promotion

Section titled “errstart: frame allocation and level promotion”
// errstart — src/backend/utils/error/elog.c
bool
errstart(int elevel, const char *domain)
{
ErrorData *edata;
bool output_to_server;
bool output_to_client = false;
if (elevel >= ERROR)
{
/* Inside a critical section → escalate to PANIC */
if (CritSectionCount > 0)
elevel = PANIC;
if (elevel == ERROR)
{
/* No handler, ExitOnAnyError, or proc_exit in progress → FATAL */
if (PG_exception_stack == NULL ||
ExitOnAnyError ||
proc_exit_inprogress)
elevel = FATAL;
}
/* Preserve any already-higher level on the stack */
for (i = 0; i <= errordata_stack_depth; i++)
elevel = Max(elevel, errordata[i].elevel);
}
output_to_server = should_output_to_server(elevel);
output_to_client = should_output_to_client(elevel);
if (elevel < ERROR && !output_to_server && !output_to_client)
return false; /* suppress: nobody wants this message */
/* ... allocate frame, set defaults, assoc_context = ErrorContext ... */
return true;
}

The level-promotion logic is the most important part of errstart. Three rules fire in sequence:

  1. Critical section → PANIC. CritSectionCount > 0 means the process holds a spinlock or LWLock in exclusive mode. Allowing a transaction abort there would leave shared memory in an inconsistent state, so the error becomes a cluster-abort.

  2. No handler / ExitOnAnyError / proc_exit → FATAL. If PG_exception_stack is NULL there is no PG_TRY handler to catch the longjmp, so the process must exit. ExitOnAnyError is used by initdb. proc_exit_inprogress prevents recursive cleanup.

  3. Honor higher severity already on stack. If an ERROR fires while a FATAL is being processed, the new error is promoted to FATAL so the process does exit.

// errfinish — src/backend/utils/error/elog.c
void
errfinish(const char *filename, int lineno, const char *funcname)
{
ErrorData *edata = &errordata[errordata_stack_depth];
int elevel;
// ... save location, switch to ErrorContext ...
/* Walk context callback stack — builds edata->context */
for (econtext = error_context_stack;
econtext != NULL;
econtext = econtext->previous)
econtext->callback(econtext->arg);
if (elevel == ERROR)
{
InterruptHoldoffCount = 0;
QueryCancelHoldoffCount = 0;
CritSectionCount = 0;
recursion_depth--;
PG_RE_THROW(); /* → siglongjmp(*PG_exception_stack, 1) */
}
EmitErrorReport(); /* format + dispatch to log / client */
FreeErrorDataContents(edata);
errordata_stack_depth--;
// ... restore context ...
if (elevel == FATAL)
{
/* ... update cumulative stats ... */
proc_exit(1);
}
if (elevel >= PANIC)
{
fflush(NULL);
abort();
}
CHECK_FOR_INTERRUPTS();
}

For ERROR, errfinish does minimal cleanup (reset interrupt holdoff counts, clear CritSectionCount) and immediately calls PG_RE_THROW(). The error frame is not freed here — it is still on the stack when the PG_CATCH block runs. The catch block must call FlushErrorState() (which resets errordata_stack_depth to −1 and calls MemoryContextReset(ErrorContext)) to reclaim the frame.

For FATAL, errfinish calls EmitErrorReport() first (so the client and log see the message), then proc_exit(1). The cumulative-statistics session-end cause is updated to DISCONNECT_FATAL if no other cause is already recorded.

For PANIC, errfinish calls fflush(NULL) (best-effort flush of all stdio buffers) and abort(). The postmaster observes the SIGABRT exit status and initiates cluster shutdown.

PG_TRY / PG_CATCH / PG_END_TRY: the guarded execution frame

Section titled “PG_TRY / PG_CATCH / PG_END_TRY: the guarded execution frame”
// PG_TRY family — src/include/utils/elog.h
#define PG_TRY(...) \
do { \
sigjmp_buf *_save_exception_stack##__VA_ARGS__ = PG_exception_stack; \
ErrorContextCallback *_save_context_stack##__VA_ARGS__ = error_context_stack; \
sigjmp_buf _local_sigjmp_buf##__VA_ARGS__; \
bool _do_rethrow##__VA_ARGS__ = false; \
if (sigsetjmp(_local_sigjmp_buf##__VA_ARGS__, 0) == 0) \
{ \
PG_exception_stack = &_local_sigjmp_buf##__VA_ARGS__
#define PG_CATCH(...) \
} \
else \
{ \
PG_exception_stack = _save_exception_stack##__VA_ARGS__; \
error_context_stack = _save_context_stack##__VA_ARGS__
#define PG_END_TRY(...) \
} \
if (_do_rethrow##__VA_ARGS__) \
PG_RE_THROW(); \
PG_exception_stack = _save_exception_stack##__VA_ARGS__; \
error_context_stack = _save_context_stack##__VA_ARGS__; \
} while (0)

The macro expansion reveals the mechanism. On entry to PG_TRY:

  1. Save the current PG_exception_stack (the enclosing handler) and error_context_stack into locals.
  2. Allocate a new sigjmp_buf on the C stack.
  3. Call sigsetjmp on it. On first entry this returns 0, so the if branch (the try body) executes; the new sigjmp_buf address is installed into PG_exception_stack.

When PG_RE_THROW() fires (i.e., siglongjmp(*PG_exception_stack, 1)):

  1. Control returns to the sigsetjmp call with return value 1 → the else branch (the catch body) runs.
  2. The first thing PG_CATCH does is restore PG_exception_stack and error_context_stack to the saved outer values. This is important: if the catch body itself throws, the outer handler catches it, not the same catch block.

PG_FINALLY is a variant that runs its block in both the normal and error path, setting _do_rethrow = true in the error path so PG_END_TRY re-throws after the finally block completes.

The optional __VA_ARGS__ suffix (available since PostgreSQL 16) allows nested PG_TRY blocks to use distinct variable names and suppress compiler -Wshadow warnings.

flowchart TD
    A["ereport(ERROR, ...)"] --> B["errstart(ERROR)"]
    B --> C{level promoted?}
    C -- "CritSectionCount>0" --> D["elevel=PANIC"]
    C -- "no PG_exception_stack" --> E["elevel=FATAL"]
    C -- "normal ERROR" --> F["allocate ErrorData frame\nassoc_context=ErrorContext"]
    F --> G["errmsg/errcode/errdetail\nbuild ErrorData fields"]
    G --> H["errfinish()"]
    H --> I["walk error_context_stack\nbuild context field"]
    I --> J["PG_RE_THROW()\nsiglongjmp(PG_exception_stack,1)"]
    J --> K["PG_CATCH block\nrestores exception_stack\nrestores context_stack"]
    K --> L["FlushErrorState()\nErrorContext reset\nstack depth = -1"]
    D --> M["EmitErrorReport\nabort()"]
    E --> N["EmitErrorReport\nproc_exit(1)"]

Figure 1 — Control flow for ereport(ERROR, ...). A normal ERROR allocates a frame, builds fields, then longjmps to the nearest PG_CATCH. PANIC and FATAL diverge at errstart level-promotion and terminate the process.

ErrorContext is a dedicated MemoryContext created early in backend startup (mcxt.c) before any user query is possible. Two invariants make it the out-of-memory escape hatch:

  1. It is never given to normal palloc requests. Its space is reserved exclusively for error message formatting.
  2. errstart sets edata->assoc_context = ErrorContext, so all errmsg / errdetail palloc calls land there rather than the current transaction context.
  3. When errstart detects re-entrant error processing (recursion_depth > 0 and elevel >= ERROR), it immediately calls MemoryContextReset(ErrorContext) to free any prior partial message, then proceeds with the inner error.

The comment in elog.c captures the design intention: “ErrorContext is guaranteed to have at least 8K of space in it (see mcxt.c), we should be able to process an ‘out of memory’ message successfully.”

ErrorContextCallback is a singly-linked list node:

// ErrorContextCallback — src/include/utils/elog.h
typedef struct ErrorContextCallback
{
struct ErrorContextCallback *previous;
void (*callback) (void *arg);
void *arg;
} ErrorContextCallback;
extern PGDLLIMPORT ErrorContextCallback *error_context_stack;

A function that wants to annotate potential errors with its current context pushes a node:

// push pattern (illustrative — used across executor, planner, etc.)
ErrorContextCallback my_callback;
my_callback.callback = my_error_context_cb;
my_callback.arg = (void *) my_state;
my_callback.previous = error_context_stack;
error_context_stack = &my_callback;
// ... do work that might ereport(ERROR) ...
error_context_stack = my_callback.previous; /* pop on normal exit */

errfinish walks error_context_stack before dispatching, calling each callback(arg). Each callback is expected to call errcontext(...), which appends to edata->context. The result appears as the CONTEXT: field in the client error message and server log. Because error_context_stack is restored by PG_CATCH before the catch block runs, callbacks that were pushed inside the PG_TRY body are automatically popped on error exit without any explicit cleanup in the catch block.

PG18 input-validation code (type input functions, JSON parsers, etc.) needs to report errors without forcing a transaction abort when called from a context that can tolerate soft failures. The errsave/ereturn mechanism was introduced for this:

// errsave / ereturn — src/include/utils/elog.h
#define errsave(context, ...) \
errsave_domain(context, TEXTDOMAIN, __VA_ARGS__)
#define ereturn(context, dummy_value, ...) \
do { \
errsave_domain(context, TEXTDOMAIN, __VA_ARGS__); \
return dummy_value; \
} while(0)

The context argument is either NULL / a non-ErrorSaveContext node (in which case errsave_start forwards to errstart(ERROR, ...) and behaves exactly like ereport(ERROR, ...)) or a pointer to an ErrorSaveContext:

// errsave_start — src/backend/utils/error/elog.c (abridged)
bool
errsave_start(struct Node *context, const char *domain)
{
ErrorSaveContext *escontext;
if (context == NULL || !IsA(context, ErrorSaveContext))
return errstart(ERROR, domain); /* fall through to normal error */
escontext = (ErrorSaveContext *) context;
escontext->error_occurred = true;
if (!escontext->details_wanted)
return false; /* caller only wants the flag, not the full ErrorData */
/* Allocate frame with elevel=LOG (signals errsave_finish this is soft) */
edata = get_error_stack_entry();
edata->elevel = LOG;
edata->assoc_context = CurrentMemoryContext; /* not ErrorContext */
return true;
}

errsave_finish packages the completed frame into escontext->error_data and returns normally — no longjmp. The caller inspects escontext->error_occurred and returns an appropriate sentinel value. This allows bulk operations (e.g., COPY FROM with ON_ERROR IGNORE) to collect errors without aborting.

Assert() is a compile-time-optional macro. With USE_ASSERT_CHECKING defined (debug builds), it expands to call ExceptionalCondition() on failure:

// Assert (USE_ASSERT_CHECKING path) — src/include/c.h
#define Assert(condition) \
do { \
if (!(condition)) \
ExceptionalCondition(#condition, __FILE__, __LINE__); \
} while (0)

ExceptionalCondition in assert.c deliberately bypasses elog() entirely — it calls write_stderr() directly, optionally dumps a backtrace via backtrace_symbols_fd(), and calls abort(). The rationale (stated in the source comment) is “wanting to minimize the amount of infrastructure that has to be working to report an assertion failure.” An assertion failure in the middle of elog.c itself must still produce output; going through elog would risk infinite recursion.

In production builds (USE_ASSERT_CHECKING not defined), Assert(condition) expands to ((void)true) — a no-op with zero runtime cost.

Emit pipeline: EmitErrorReport and log destinations

Section titled “Emit pipeline: EmitErrorReport and log destinations”

EmitErrorReport (called from errfinish for non-ERROR levels, and from PostgresMain after catching an ERROR) dispatches to two sinks:

// EmitErrorReport — src/backend/utils/error/elog.c (abridged)
void
EmitErrorReport(void)
{
// ...
if (edata->output_to_server && emit_log_hook)
(*emit_log_hook)(edata); /* extension hook — first */
if (edata->output_to_server)
send_message_to_server_log(edata);
if (edata->output_to_client)
send_message_to_frontend(edata);
// ...
}

emit_log_hook fires first, allowing an extension to suppress or redirect server-log output. The hook can turn off edata->output_to_server but cannot turn it on (because messages suppressed by log_min_messages never reach EmitErrorReport in the first place).

send_message_to_server_log formats the line using log_line_prefix and dispatches to one or more log destinations: stderr (always available), syslog (Unix only, HAVE_SYSLOG), Windows event log (WIN32), CSV log (csvlog.c / write_csvlog), and JSON log (jsonlog.c / write_jsonlog). The Log_destination GUC is a bitmap (LOG_DESTINATION_STDERR = 1, LOG_DESTINATION_SYSLOG = 2, LOG_DESTINATION_EVENTLOG = 4, LOG_DESTINATION_CSVLOG = 8, LOG_DESTINATION_JSONLOG = 16).

send_message_to_frontend formats the message as a PostgreSQL wire-protocol E (ErrorResponse) or N (NoticeResponse) message via pqformat.c.

flowchart LR
    E["EmitErrorReport()"] --> H["emit_log_hook\n(extension, optional)"]
    H --> SL["send_message_to_server_log"]
    E --> FE["send_message_to_frontend\n(wire protocol E/N)"]
    SL --> STDERR["stderr"]
    SL --> SYSLOG["syslog\n(HAVE_SYSLOG)"]
    SL --> EVENTLOG["eventlog\n(WIN32)"]
    SL --> CSV["write_csvlog"]
    SL --> JSON["write_jsonlog"]

Figure 2 — Emit pipeline from EmitErrorReport. The hook fires before the server-log path. The frontend path is independent and always uses the wire protocol. Log destinations are a GUC bitmap.

  • errstart (elog.c) — entry gate; decides severity, allocates ErrorData frame, returns false to suppress low-severity messages.
  • errstart_cold (elog.c) — pg_attribute_cold wrapper around errstart for branch-prediction optimization.
  • errfinish (elog.c) — completes the frame, walks context callbacks, then either PG_RE_THROW() (ERROR), proc_exit(1) (FATAL), or abort() (PANIC).
  • get_error_stack_entry (elog.c) — internal; increments errordata_stack_depth, panics on overflow, memsets the new frame.
  • EmitErrorReport (elog.c) — calls emit_log_hook then dispatches to server log and/or frontend.
  • send_message_to_server_log (elog.c) — formats with log_line_prefix, routes to stderr / syslog / eventlog / csvlog / jsonlog.
  • send_message_to_frontend (elog.c) — formats as wire-protocol E or N message.
  • pg_re_throw (elog.c) — siglongjmp(*PG_exception_stack, 1); if PG_exception_stack is NULL, promotes to FATAL and calls errfinish.
  • errcode (elog.c) — sets edata->sqlerrcode.
  • errcode_for_file_access (elog.c) — maps saved_errno to file-access SQLSTATE.
  • errcode_for_socket_access (elog.c) — maps saved_errno to socket SQLSTATE.
  • errmsg / errmsg_internal / errmsg_plural (elog.c) — set edata->message via EVALUATE_MESSAGE.
  • errdetail / errdetail_internal / errdetail_log (elog.c) — set edata->detail or edata->detail_log.
  • errhint / errhint_internal (elog.c) — set edata->hint.
  • errcontext_msg / set_errcontext_domain (elog.c) — append to edata->context (multiple calls accumulate).
  • errposition / internalerrposition / internalerrquery (elog.c) — set cursor position fields.
  • errhidestmt / errhidecontext (elog.c) — suppress STATEMENT / CONTEXT from log output.
  • errbacktrace / set_backtrace (elog.c) — collect and attach a backtrace_symbols string for debugging.
  • CopyErrorData (elog.c) — deep-copies topmost frame to caller’s context; used in catch blocks that need to examine the error after FlushErrorState.
  • FreeErrorData (elog.c) — frees a CopyErrorData result.
  • FlushErrorState (elog.c) — resets errordata_stack_depth to −1 and calls MemoryContextReset(ErrorContext); must be called by catch blocks before returning to normal processing.
  • ThrowErrorData (elog.c) — re-reports a previously copied ErrorData as a new ereport cycle; used to propagate background-worker errors.
  • ReThrowError (elog.c) — pushes a copied ErrorData back onto the stack and calls PG_RE_THROW(); used when a catch block needs to do work before re-raising.
  • errsave_start (elog.c) — entry gate for errsave(); checks for ErrorSaveContext and either forwards to errstart(ERROR) or allocates a soft frame with elevel=LOG.
  • errsave_finish (elog.c) — packages completed soft frame into escontext->error_data and returns normally (no longjmp).
  • ExceptionalCondition (assert.c) — called by Assert() on failure; writes to stderr, optionally dumps backtrace, calls abort().
  • Assert / AssertMacro (c.h) — macro; no-op in production builds, calls ExceptionalCondition in assert-checking builds.
  • ErrorData (elog.h) — the error frame struct.
  • ErrorContextCallback (elog.h) — context callback linked-list node.
  • PG_TRY / PG_CATCH / PG_FINALLY / PG_END_TRY / PG_RE_THROW (elog.h) — the guarded execution macros.
  • ereport / ereport_domain / elog (elog.h) — the call-site macros.
  • errsave / errsave_domain / ereturn / ereturn_domain (elog.h) — soft-error macros.
  • ERRORDATA_STACK_SIZE (elog.c) — stack depth constant (5).
  • PG_exception_stack (elog.c) — global sigjmp_buf *; NULL means no handler installed.
  • error_context_stack (elog.c) — global ErrorContextCallback *; head of the callback list.
  • emit_log_hook (elog.c) — global function pointer for log interception.

Position hints (as of 2026-06-05, commit 273fe94)

Section titled “Position hints (as of 2026-06-05, commit 273fe94)”
SymbolFileLine
DEBUG5src/include/utils/elog.h26
ERRORsrc/include/utils/elog.h52
FATALsrc/include/utils/elog.h55
PANICsrc/include/utils/elog.h56
ereport macrosrc/include/utils/elog.h163
elog macrosrc/include/utils/elog.h239
errsave macrosrc/include/utils/elog.h275
ErrorContextCallback typedefsrc/include/utils/elog.h308
PG_TRY macrosrc/include/utils/elog.h385
PG_CATCH macrosrc/include/utils/elog.h395
PG_END_TRY macrosrc/include/utils/elog.h410
PG_RE_THROW macrosrc/include/utils/elog.h418
ErrorData typedefsrc/include/utils/elog.h432
Assert macro (production no-op)src/include/c.h837
Assert macro (assert-checking)src/include/c.h852
ERRORDATA_STACK_SIZEsrc/backend/utils/error/elog.c144
message_level_is_interestingsrc/backend/utils/error/elog.c273
in_error_recursion_troublesrc/backend/utils/error/elog.c294
errstart_coldsrc/backend/utils/error/elog.c327
errstartsrc/backend/utils/error/elog.c343
errfinishsrc/backend/utils/error/elog.c474
errsave_startsrc/backend/utils/error/elog.c630
errsave_finishsrc/backend/utils/error/elog.c682
EmitErrorReportsrc/backend/utils/error/elog.c1692
CopyErrorDatasrc/backend/utils/error/elog.c1751
FreeErrorDatasrc/backend/utils/error/elog.c1823
FlushErrorStatesrc/backend/utils/error/elog.c1872
ThrowErrorDatasrc/backend/utils/error/elog.c1900
ReThrowErrorsrc/backend/utils/error/elog.c1959
pg_re_throwsrc/backend/utils/error/elog.c2009
GetErrorContextStacksrc/backend/utils/error/elog.c2064
send_message_to_server_logsrc/backend/utils/error/elog.c3230
send_message_to_frontendsrc/backend/utils/error/elog.c3533
ExceptionalConditionsrc/backend/utils/error/assert.c30
  • ERRORDATA_STACK_SIZE is 5, hard-coded. Verified at elog.c:144. The overflow handler resets depth to −1 and fires ereport(PANIC, ...) to self-bootstrap one free slot — a deliberate strategy for handling “error during error processing.”

  • errstart performs level promotion before frame allocation. The CritSectionCount > 0 → PANIC, PG_exception_stack == NULL → FATAL, and Max(existing stack level) promotions all occur before get_error_stack_entry is called. Verified by reading elog.c:343–472.

  • The errsave soft-error path was present in REL_18_STABLE. errsave_start (elog.c:630) and errsave_finish (elog.c:682) are present and fully implemented. ErrorSaveContext is in src/include/nodes/miscnodes.h. The ON_ERROR IGNORE COPY path is one caller.

  • Assert() in production builds is a strict no-op. The c.h:837 branch (#ifndef USE_ASSERT_CHECKING) expands to ((void)true). No function call, no branch. Verified by reading src/include/c.h:835–868.

  • emit_log_hook fires before send_message_to_server_log, not after. The hook can suppress server-log output (set output_to_server = false) but cannot add output for messages that errstart already suppressed. Verified at elog.c:1692–1745.

  • PG_CATCH restores both PG_exception_stack and error_context_stack immediately on entry. The macro expansion (verified at elog.h:395–408) shows that the first two assignments in the else branch restore both globals. Context callbacks pushed inside the PG_TRY body are therefore invisible to the catch block without any explicit pop.

  • pg_re_throw promotes to FATAL if PG_exception_stack is NULL. Verified at elog.c:2009–2060. This handles the case where ereport(ERROR) fires inside a PG_TRY block that was subsequently exited without the error being caught — the code exits the PG_TRY normally, then discovers the outer PG_exception_stack is gone.

  • ExceptionalCondition deliberately does not call elog. The assert.c source comment states this explicitly, and the implementation (write_stderr → optional backtrace_symbols_fdabort()) confirms it. Verified at assert.c:30–67.

  1. WARNING_CLIENT_ONLY (level 20) routing. should_output_to_server calls is_log_level_output which explicitly returns false for WARNING_CLIENT_ONLY, confirmed at elog.c (the elevel == WARNING_CLIENT_ONLY branch). However, it is not obvious which callers use this level vs. plain WARNING. A grep of the REL_18 tree would identify the use sites and clarify the intended semantic.

  2. errsave and ErrorSaveContext.details_wanted = false. When details_wanted is false, errsave_start sets error_occurred = true and returns false immediately, skipping all field builders. The caller then has only a boolean signal. It is unclear from the code alone which callers set details_wanted = false vs. true and whether there is a documented policy for choosing. Investigation path: grep ErrorSaveContext initializations across src/backend/.

  3. CritSectionCount vs. LWLock critical sections. The CritSectionCount check in errstart escalates to PANIC. It is incremented by START_CRIT_SECTION() (which holds spinlocks and LWLocks), but not by LWLockAcquire alone. Whether LWLockAcquire itself increments CritSectionCount in PG18 requires a grep of lwlock.c. If it does not, then an ERROR fired while holding an LWLock (not inside a crit section) would not be promoted to PANIC — and the shared-memory corruption concern would depend on the specific lock.

Beyond PostgreSQL — Comparative Designs & Research Frontiers

Section titled “Beyond PostgreSQL — Comparative Designs & Research Frontiers”
  • C++ exception-based engines (MySQL InnoDB, RocksDB). These use native throw/catch, which provides automatic RAII-style cleanup through destructors. PostgreSQL’s longjmp approach predates C++ and requires explicit cleanup discipline (memory contexts, resource owners). A comparison of the cleanup overhead and the “no destructor” tax would quantify the cost/benefit of the C approach.

  • Soft errors and non-aborting pipelines. PostgreSQL’s errsave/ereturn (PG14+) enables non-aborting input validation. Oracle Database has similar per-row error handling via SAVE EXCEPTIONS in bulk DML. A side-by-side would illuminate design tradeoffs: PostgreSQL’s approach is call-depth-local (the caller inspects the ErrorSaveContext); Oracle’s is set-based (a bulk error log table). The postgres-executor.md COPY ON_ERROR IGNORE analysis is the natural companion.

  • Structured logging and log destinations. PostgreSQL’s csvlog and jsonlog destinations (write_csvlog, write_jsonlog) output structured records that are amenable to ingestion by Elasticsearch/Loki. Comparing with SQL Server’s Extended Events or Oracle’s Unified Audit Trail would highlight the tradeoff between in-process structured logging and external log pipelines.

  • setjmp/longjmp vs. ucontext / fibers. PostgreSQL’s coroutine primitives (src/backend/tcop/postgres.c signal handling) rely on sigsetjmp. An evolution to fiber-based or async I/O (PG18 storage/aio/) raises the question of whether PG_exception_stack must become fiber-local. The postgres-aio.md doc (planned, P2) will cover the interaction.

  • Error codes and SQLSTATE coverage. PostgreSQL’s src/backend/utils/errcodes.txt defines several hundred SQLSTATE codes. Research on whether real applications use fine-grained SQLSTATE dispatch (DECLARE ... HANDLER FOR SQLSTATE '... in PL/pgSQL) or collapse to class-level handling would inform whether the granularity of errcode_for_file_access / errcode_for_socket_access is practically used or vestigial.

Source code (REL_18_STABLE, commit 273fe94)

Section titled “Source code (REL_18_STABLE, commit 273fe94)”
  • src/backend/utils/error/elog.c — core pipeline (3,826 lines)
  • src/backend/utils/error/assert.cExceptionalCondition (67 lines)
  • src/backend/utils/error/csvlog.c — CSV log destination
  • src/backend/utils/error/jsonlog.c — JSON log destination
  • src/include/utils/elog.h — public API, macros, ErrorData, PG_TRY family
  • src/include/c.hAssert / AssertMacro macros
  • src/include/nodes/miscnodes.hErrorSaveContext node definition
  • Database Internals (Petrov, 2019) — memory management and crash recovery framing; relevant to the ErrorContext reserved-space strategy.
  • Database System Concepts (Silberschatz et al., 7e) — SQL error/condition model; SQLSTATE class codes.
  • postgres-memory-contexts.mdErrorContext is a MemoryContext; the delete-cascade mechanism is how ERROR recovery reclaims memory.
  • postgres-resource-owners.md — non-memory resources (buffer pins, relation locks, file descriptors) released on error via ResourceOwner abort.
  • postgres-xact.md — transaction abort driven by the ERROR recovery path; AbortTransaction is called from PostgresMain after PG_CATCH.
  • postgres-lock-manager.md — lock release on transaction abort interacts with the PG_TRY/PG_CATCH discipline.