Skip to main content

Python/errors.c

Source:

cpython 3.14 @ ab2d84fe1023/Python/errors.c

errors.c is the authoritative implementation of CPython's exception state machine. It owns the six core operations (set, format, occurred, clear, fetch, restore), the chaining logic that wires __cause__ and __context__, the unraisable error reporter used in destructors and finalizers, and the per-thread exception state embedded in PyThreadState.

Map

LinesSymbolPurpose
1–40file prologueIncludes, _PyErr_StackItem forward reference
41–110PyErr_SetObjectCore setter: validate type, normalise to instance, write to thread state
111–145PyErr_SetStringThin wrapper: intern message, call PyErr_SetObject
146–210PyErr_Format / _PyErr_FormatVprintf-style exception message builder
211–265PyErr_OccurredFast-path read of tstate->current_exception
266–310PyErr_ClearDrop current exception and decref
311–390PyErr_Fetch / PyErr_RestoreLegacy triple (type, value, traceback) compatibility shim
391–480_PyErr_NormalizeExceptionEnsure the exception value is an instance of the type
481–570PyErr_SetExcInfo / PyErr_GetExcInfosys.exc_info() thread-state accessors
571–680_PyErr_ChainExceptionsSet __context__; wire __cause__ and __suppress_context__
681–780PyErr_WriteUnraisableEmit unraisable exception to sys.unraisablehook
781–900PyErr_CheckSignals / PyErr_SetInterruptSignal-driven KeyboardInterrupt injection
901–1050PyErr_NewException / PyErr_NewExceptionWithDocRuntime exception class factory
1051–1200_PyErr_ProgramDecodedTextObject and friendsSource-line fetch for tracebacks

Reading

Setting and clearing exceptions

PyErr_SetObject is the primitive all other setters delegate to. It writes a single PyObject * into the thread state rather than the legacy (type, value, tb) triple that CPython used through 3.11:

// CPython: Python/errors.c:41 PyErr_SetObject
void
PyErr_SetObject(PyObject *exception, PyObject *value)
{
PyThreadState *tstate = _PyThreadState_GET();
/* Normalise: if value is not already an instance of exception,
call exception(value) to produce one. */
if (value != NULL && !PyExceptionInstance_Check(value)) {
PyObject *fixed = _PyObject_CallOneArg(exception, value);
if (fixed == NULL) {
return; /* recursive error; leave it */
}
value = fixed;
}
PyObject *old = tstate->current_exception;
tstate->current_exception = Py_XNewRef(value);
Py_XDECREF(old);
}

current_exception holds a strong reference to a normalised exception instance. There is no separate type or traceback field in 3.12+ thread state; the type is always Py_TYPE(current_exception) and the traceback is stored on the exception instance itself as __traceback__.

PyErr_Clear is a one-liner over the same field:

// CPython: Python/errors.c:266 PyErr_Clear
void
PyErr_Clear(void)
{
PyThreadState *tstate = _PyThreadState_GET();
PyObject *old = tstate->current_exception;
tstate->current_exception = NULL;
Py_XDECREF(old);
}

PyErr_Occurred is called on essentially every opcode boundary in the eval loop, so it is kept as a branchless pointer read:

// CPython: Python/errors.c:211 PyErr_Occurred
PyObject *
PyErr_Occurred(void)
{
PyThreadState *tstate = _PyThreadState_GET();
return tstate->current_exception;
}

Fetch/Restore and the legacy triple shim

Public C extensions written before 3.12 use PyErr_Fetch and PyErr_Restore to save and restore exception state across calls. These now translate between the new single-field model and the old (type, value, traceback) triple:

// CPython: Python/errors.c:311 PyErr_Fetch
void
PyErr_Fetch(PyObject **p_type, PyObject **p_value, PyObject **p_traceback)
{
PyThreadState *tstate = _PyThreadState_GET();
PyObject *exc = tstate->current_exception;
tstate->current_exception = NULL;
if (exc == NULL) {
*p_type = NULL; *p_value = NULL; *p_traceback = NULL;
return;
}
*p_type = Py_NewRef(Py_TYPE(exc));
*p_value = exc; /* caller owns the instance ref */
*p_traceback = Py_XNewRef(PyException_GetTraceback(exc));
}

PyErr_Restore is the inverse: it drops p_type and p_traceback immediately (they are redundant with the instance) and installs p_value as the new current_exception:

// CPython: Python/errors.c:345 PyErr_Restore
void
PyErr_Restore(PyObject *type, PyObject *value, PyObject *traceback)
{
PyThreadState *tstate = _PyThreadState_GET();
if (traceback != NULL) {
PyException_SetTraceback(value, traceback);
Py_DECREF(traceback);
}
Py_XDECREF(type);
PyObject *old = tstate->current_exception;
tstate->current_exception = value; /* steal ref */
Py_XDECREF(old);
}

Exception chaining with _PyErr_ChainExceptions

When an exception is raised inside an except block, Python 3 automatically sets __context__ to the active exception. When raise X from Y is used, __cause__ is set instead and __suppress_context__ is flipped to True:

// CPython: Python/errors.c:571 _PyErr_ChainExceptions
void
_PyErr_ChainExceptions(PyObject *exc, PyObject *context)
{
if (exc == NULL || context == NULL) {
Py_XDECREF(context);
return;
}
/* Walk the __context__ chain to avoid creating a cycle. */
PyObject *cur = exc;
while (1) {
PyObject *ctx = PyException_GetContext(cur);
if (ctx == NULL) break;
if (ctx == exc) {
/* Cycle detected; do not chain. */
Py_DECREF(ctx);
break;
}
Py_DECREF(ctx);
cur = ctx;
}
PyException_SetContext(exc, Py_NewRef(context));
Py_DECREF(context);
}

The cycle-detection walk ensures that a re-raised exception whose __context__ chain eventually leads back to itself does not produce an infinite loop in traceback printing. _PyErr_SetImplicitExceptionContext (called from ceval.c) wraps this function and also handles the __suppress_context__ flag for explicit raise ... from ... forms.

PyErr_WriteUnraisable

Destructors (__del__), weak-reference callbacks, and threading.Thread run in contexts where propagating an exception is impossible. CPython routes these through PyErr_WriteUnraisable:

// CPython: Python/errors.c:681 PyErr_WriteUnraisable
void
PyErr_WriteUnraisable(PyObject *obj)
{
PyThreadState *tstate = _PyThreadState_GET();
PyObject *exc = tstate->current_exception;
tstate->current_exception = NULL;
if (exc == NULL) {
return;
}
/* Build an _UnraisableHookArgs namedtuple and invoke sys.unraisablehook. */
PyObject *hook = _PySys_GetAttr(tstate, &_Py_ID(unraisablehook));
if (hook != NULL) {
PyObject *args = make_unraisable_hook_args(tstate, exc, obj);
if (args != NULL) {
PyObject *res = PyObject_CallOneArg(hook, args);
Py_XDECREF(res);
Py_DECREF(args);
}
}
Py_DECREF(exc);
}

If sys.unraisablehook itself raises, CPython prints a hard-coded message to sys.stderr and clears both exceptions to guarantee forward progress. The obj argument is the object associated with the failing operation (the instance whose __del__ failed, the weak-ref callback, etc.) and appears in the hook args as object.

gopy notes

Status: not yet ported.

Planned package path: vm/errors.go (or objects/errors.go depending on import layering). The exception state field current_exception corresponds to the exc field already tracked on the thread/goroutine state in vm/eval_gen.go. PyErr_SetObject, PyErr_Clear, and PyErr_Occurred are the first three functions to port because every opcode that can raise an exception depends on them.

PyErr_Fetch/PyErr_Restore need compatibility wrappers for any C-extension bridge code, but the internal eval loop will use the single-field model from day one.

_PyErr_ChainExceptions should land alongside RAISE_VARARGS opcode support (already tracked in vm/eval_unwind.go). PyErr_WriteUnraisable is needed before __del__ finalisation is enabled.