PostgreSQL Error Handling — ereport/elog, PG_TRY/sigsetjmp, and the ErrorContext Escape Hatch
Contents:
- Theoretical Background
- Common DBMS Design
- PostgreSQL’s Approach
- Source Walkthrough
- Source verification (as of 2026-06-05)
- Beyond PostgreSQL — Comparative Designs & Research Frontiers
- Sources
Theoretical Background
Section titled “Theoretical Background”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:
- Non-local (escapes arbitrary call depth without C exception support).
- Structured (carries SQLSTATE, message fields, source location).
- 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.
Common DBMS Design
Section titled “Common DBMS Design”This section names the engineering patterns that appear across most production-grade database engines before reaching PostgreSQL’s specific choices.
Two-phase error reporting
Section titled “Two-phase error reporting”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.
Error stack for re-entrant reporting
Section titled “Error stack for re-entrant reporting”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.
Dedicated error-reporting memory context
Section titled “Dedicated error-reporting memory context”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.
setjmp/longjmp for ERROR-level recovery
Section titled “setjmp/longjmp for ERROR-level recovery”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 ↔ PostgreSQL mapping
Section titled “Theory ↔ PostgreSQL mapping”| Theory / convention | PostgreSQL name |
|---|---|
| Begin call (allocate error frame) | errstart() / errstart_cold() |
| Finish call (dispatch + recover) | errfinish() |
| Error frame | ErrorData struct |
| Error frame stack | errordata[ERRORDATA_STACK_SIZE] (depth 5) |
| Reserved error memory | ErrorContext (MemoryContext) |
setjmp handler pointer | PG_exception_stack (sigjmp_buf *) |
| Guarded execution site | PG_TRY() macro (sigsetjmp) |
| Non-local transfer to handler | PG_RE_THROW() → pg_re_throw() → siglongjmp |
| Context callback stack | error_context_stack (ErrorContextCallback *) |
| Structured error payload | ErrorData fields: sqlerrcode, message, detail, hint, context, … |
| SQLSTATE code | 30-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 path | errsave() / ereturn() + ErrorSaveContext |
| Assertion failure (bypasses elog) | Assert() → ExceptionalCondition() → abort() |
PostgreSQL’s Approach
Section titled “PostgreSQL’s Approach”Severity levels and the severity axis
Section titled “Severity levels and the severity axis”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.
The ErrorData struct
Section titled “The ErrorData struct”Each in-flight error occupies one slot in the errordata[] array:
// ErrorData — src/include/utils/elog.htypedef 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.”
ereport / elog: the call-site macros
Section titled “ereport / elog: the call-site macros”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.cboolerrstart(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:
-
Critical section → PANIC.
CritSectionCount > 0means 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. -
No handler / ExitOnAnyError / proc_exit → FATAL. If
PG_exception_stackisNULLthere is noPG_TRYhandler to catch thelongjmp, so the process must exit.ExitOnAnyErroris used byinitdb.proc_exit_inprogressprevents recursive cleanup. -
Honor higher severity already on stack. If an
ERRORfires while aFATALis being processed, the new error is promoted toFATALso the process does exit.
errfinish: dispatch and control transfer
Section titled “errfinish: dispatch and control transfer”// errfinish — src/backend/utils/error/elog.cvoiderrfinish(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:
- Save the current
PG_exception_stack(the enclosing handler) anderror_context_stackinto locals. - Allocate a new
sigjmp_bufon the C stack. - Call
sigsetjmpon it. On first entry this returns 0, so theifbranch (the try body) executes; the newsigjmp_bufaddress is installed intoPG_exception_stack.
When PG_RE_THROW() fires (i.e., siglongjmp(*PG_exception_stack, 1)):
- Control returns to the
sigsetjmpcall with return value 1 → theelsebranch (the catch body) runs. - The first thing
PG_CATCHdoes is restorePG_exception_stackanderror_context_stackto 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: the reserved escape hatch
Section titled “ErrorContext: the reserved escape hatch”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:
- It is never given to normal palloc requests. Its space is reserved exclusively for error message formatting.
errstartsetsedata->assoc_context = ErrorContext, so allerrmsg/errdetailpalloc calls land there rather than the current transaction context.- When
errstartdetects re-entrant error processing (recursion_depth > 0andelevel >= ERROR), it immediately callsMemoryContextReset(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.”
Context callbacks: call-stack narration
Section titled “Context callbacks: call-stack narration”ErrorContextCallback is a singly-linked list node:
// ErrorContextCallback — src/include/utils/elog.htypedef 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.
Soft errors: errsave / ereturn
Section titled “Soft errors: errsave / ereturn”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)boolerrsave_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 and ExceptionalCondition
Section titled “Assert and ExceptionalCondition”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)voidEmitErrorReport(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.
Source Walkthrough
Section titled “Source Walkthrough”Core pipeline
Section titled “Core pipeline”errstart(elog.c) — entry gate; decides severity, allocatesErrorDataframe, returns false to suppress low-severity messages.errstart_cold(elog.c) —pg_attribute_coldwrapper arounderrstartfor branch-prediction optimization.errfinish(elog.c) — completes the frame, walks context callbacks, then eitherPG_RE_THROW()(ERROR),proc_exit(1)(FATAL), orabort()(PANIC).get_error_stack_entry(elog.c) — internal; incrementserrordata_stack_depth, panics on overflow, memsets the new frame.EmitErrorReport(elog.c) — callsemit_log_hookthen dispatches to server log and/or frontend.send_message_to_server_log(elog.c) — formats withlog_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); ifPG_exception_stackisNULL, promotes to FATAL and callserrfinish.
Frame field builders
Section titled “Frame field builders”errcode(elog.c) — setsedata->sqlerrcode.errcode_for_file_access(elog.c) — mapssaved_errnoto file-access SQLSTATE.errcode_for_socket_access(elog.c) — mapssaved_errnoto socket SQLSTATE.errmsg/errmsg_internal/errmsg_plural(elog.c) — setedata->messageviaEVALUATE_MESSAGE.errdetail/errdetail_internal/errdetail_log(elog.c) — setedata->detailoredata->detail_log.errhint/errhint_internal(elog.c) — setedata->hint.errcontext_msg/set_errcontext_domain(elog.c) — append toedata->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 abacktrace_symbolsstring for debugging.
Error state management
Section titled “Error state management”CopyErrorData(elog.c) — deep-copies topmost frame to caller’s context; used in catch blocks that need to examine the error afterFlushErrorState.FreeErrorData(elog.c) — frees aCopyErrorDataresult.FlushErrorState(elog.c) — resetserrordata_stack_depthto −1 and callsMemoryContextReset(ErrorContext); must be called by catch blocks before returning to normal processing.ThrowErrorData(elog.c) — re-reports a previously copiedErrorDataas a newereportcycle; used to propagate background-worker errors.ReThrowError(elog.c) — pushes a copiedErrorDataback onto the stack and callsPG_RE_THROW(); used when a catch block needs to do work before re-raising.
Soft error path
Section titled “Soft error path”errsave_start(elog.c) — entry gate forerrsave(); checks forErrorSaveContextand either forwards toerrstart(ERROR)or allocates a soft frame withelevel=LOG.errsave_finish(elog.c) — packages completed soft frame intoescontext->error_dataand returns normally (no longjmp).
Assert path
Section titled “Assert path”ExceptionalCondition(assert.c) — called byAssert()on failure; writes to stderr, optionally dumps backtrace, callsabort().Assert/AssertMacro(c.h) — macro; no-op in production builds, callsExceptionalConditionin assert-checking builds.
Data types and macros
Section titled “Data types and macros”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) — globalsigjmp_buf *; NULL means no handler installed.error_context_stack(elog.c) — globalErrorContextCallback *; 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)”| Symbol | File | Line |
|---|---|---|
DEBUG5 | src/include/utils/elog.h | 26 |
ERROR | src/include/utils/elog.h | 52 |
FATAL | src/include/utils/elog.h | 55 |
PANIC | src/include/utils/elog.h | 56 |
ereport macro | src/include/utils/elog.h | 163 |
elog macro | src/include/utils/elog.h | 239 |
errsave macro | src/include/utils/elog.h | 275 |
ErrorContextCallback typedef | src/include/utils/elog.h | 308 |
PG_TRY macro | src/include/utils/elog.h | 385 |
PG_CATCH macro | src/include/utils/elog.h | 395 |
PG_END_TRY macro | src/include/utils/elog.h | 410 |
PG_RE_THROW macro | src/include/utils/elog.h | 418 |
ErrorData typedef | src/include/utils/elog.h | 432 |
Assert macro (production no-op) | src/include/c.h | 837 |
Assert macro (assert-checking) | src/include/c.h | 852 |
ERRORDATA_STACK_SIZE | src/backend/utils/error/elog.c | 144 |
message_level_is_interesting | src/backend/utils/error/elog.c | 273 |
in_error_recursion_trouble | src/backend/utils/error/elog.c | 294 |
errstart_cold | src/backend/utils/error/elog.c | 327 |
errstart | src/backend/utils/error/elog.c | 343 |
errfinish | src/backend/utils/error/elog.c | 474 |
errsave_start | src/backend/utils/error/elog.c | 630 |
errsave_finish | src/backend/utils/error/elog.c | 682 |
EmitErrorReport | src/backend/utils/error/elog.c | 1692 |
CopyErrorData | src/backend/utils/error/elog.c | 1751 |
FreeErrorData | src/backend/utils/error/elog.c | 1823 |
FlushErrorState | src/backend/utils/error/elog.c | 1872 |
ThrowErrorData | src/backend/utils/error/elog.c | 1900 |
ReThrowError | src/backend/utils/error/elog.c | 1959 |
pg_re_throw | src/backend/utils/error/elog.c | 2009 |
GetErrorContextStack | src/backend/utils/error/elog.c | 2064 |
send_message_to_server_log | src/backend/utils/error/elog.c | 3230 |
send_message_to_frontend | src/backend/utils/error/elog.c | 3533 |
ExceptionalCondition | src/backend/utils/error/assert.c | 30 |
Source verification (as of 2026-06-05)
Section titled “Source verification (as of 2026-06-05)”Verified facts
Section titled “Verified facts”-
ERRORDATA_STACK_SIZEis 5, hard-coded. Verified atelog.c:144. The overflow handler resets depth to −1 and firesereport(PANIC, ...)to self-bootstrap one free slot — a deliberate strategy for handling “error during error processing.” -
errstartperforms level promotion before frame allocation. TheCritSectionCount > 0 → PANIC,PG_exception_stack == NULL → FATAL, andMax(existing stack level)promotions all occur beforeget_error_stack_entryis called. Verified by readingelog.c:343–472. -
The
errsavesoft-error path was present in REL_18_STABLE.errsave_start(elog.c:630) anderrsave_finish(elog.c:682) are present and fully implemented.ErrorSaveContextis insrc/include/nodes/miscnodes.h. TheON_ERROR IGNORECOPY path is one caller. -
Assert()in production builds is a strict no-op. Thec.h:837branch (#ifndef USE_ASSERT_CHECKING) expands to((void)true). No function call, no branch. Verified by readingsrc/include/c.h:835–868. -
emit_log_hookfires beforesend_message_to_server_log, not after. The hook can suppress server-log output (setoutput_to_server = false) but cannot add output for messages thaterrstartalready suppressed. Verified atelog.c:1692–1745. -
PG_CATCHrestores bothPG_exception_stackanderror_context_stackimmediately on entry. The macro expansion (verified atelog.h:395–408) shows that the first two assignments in theelsebranch restore both globals. Context callbacks pushed inside thePG_TRYbody are therefore invisible to the catch block without any explicit pop. -
pg_re_throwpromotes to FATAL ifPG_exception_stackis NULL. Verified atelog.c:2009–2060. This handles the case whereereport(ERROR)fires inside aPG_TRYblock that was subsequently exited without the error being caught — the code exits thePG_TRYnormally, then discovers the outerPG_exception_stackis gone. -
ExceptionalConditiondeliberately does not callelog. Theassert.csource comment states this explicitly, and the implementation (write_stderr→ optionalbacktrace_symbols_fd→abort()) confirms it. Verified atassert.c:30–67.
Open questions
Section titled “Open questions”-
WARNING_CLIENT_ONLY(level 20) routing.should_output_to_servercallsis_log_level_outputwhich explicitly returnsfalseforWARNING_CLIENT_ONLY, confirmed atelog.c(theelevel == WARNING_CLIENT_ONLYbranch). However, it is not obvious which callers use this level vs. plainWARNING. A grep of the REL_18 tree would identify the use sites and clarify the intended semantic. -
errsaveandErrorSaveContext.details_wanted = false. Whendetails_wantedis false,errsave_startsetserror_occurred = trueand returns false immediately, skipping all field builders. The caller then has only a boolean signal. It is unclear from the code alone which callers setdetails_wanted = falsevs. true and whether there is a documented policy for choosing. Investigation path: grepErrorSaveContextinitializations acrosssrc/backend/. -
CritSectionCountvs.LWLockcritical sections. TheCritSectionCountcheck inerrstartescalates to PANIC. It is incremented bySTART_CRIT_SECTION()(which holds spinlocks and LWLocks), but not byLWLockAcquirealone. WhetherLWLockAcquireitself incrementsCritSectionCountin PG18 requires a grep oflwlock.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’slongjmpapproach 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 viaSAVE EXCEPTIONSin bulk DML. A side-by-side would illuminate design tradeoffs: PostgreSQL’s approach is call-depth-local (the caller inspects theErrorSaveContext); Oracle’s is set-based (a bulk error log table). Thepostgres-executor.mdCOPY ON_ERROR IGNOREanalysis is the natural companion. -
Structured logging and log destinations. PostgreSQL’s
csvlogandjsonlogdestinations (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/longjmpvs.ucontext/ fibers. PostgreSQL’s coroutine primitives (src/backend/tcop/postgres.csignal handling) rely onsigsetjmp. An evolution to fiber-based or async I/O (PG18storage/aio/) raises the question of whetherPG_exception_stackmust become fiber-local. Thepostgres-aio.mddoc (planned, P2) will cover the interaction. -
Error codes and SQLSTATE coverage. PostgreSQL’s
src/backend/utils/errcodes.txtdefines 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 oferrcode_for_file_access/errcode_for_socket_accessis practically used or vestigial.
Sources
Section titled “Sources”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.c—ExceptionalCondition(67 lines)src/backend/utils/error/csvlog.c— CSV log destinationsrc/backend/utils/error/jsonlog.c— JSON log destinationsrc/include/utils/elog.h— public API, macros,ErrorData,PG_TRYfamilysrc/include/c.h—Assert/AssertMacromacrossrc/include/nodes/miscnodes.h—ErrorSaveContextnode definition
Textbooks
Section titled “Textbooks”- 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.
Cross-references in this KB
Section titled “Cross-references in this KB”postgres-memory-contexts.md—ErrorContextis aMemoryContext; 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 viaResourceOwnerabort.postgres-xact.md— transaction abort driven by the ERROR recovery path;AbortTransactionis called fromPostgresMainafterPG_CATCH.postgres-lock-manager.md— lock release on transaction abort interacts with thePG_TRY/PG_CATCHdiscipline.