How std::uncaught_exceptions Detects Active Exceptions
This article explains how the C++ standard library function
std::uncaught_exceptions monitors active exception states
during program execution. Introduced in C++17 to replace the deprecated
boolean std::uncaught_exception, this function returns the
exact number of uncaught exception objects currently active in the
calling thread. Below, we examine the underlying runtime mechanisms, the
thread-local state tracking employed by modern C++ implementations, and
how this integer count enables robust, exception-safe resource
management.
The Problem with Boolean State Tracking
Prior to C++17, the standard provided
std::uncaught_exception(), which returned a simple
bool. This boolean returned true if an
exception had been thrown and was currently unwinding the stack, and
false otherwise.
However, this design failed during nested exception scenarios. If a
destructor executing during stack unwinding declared its own internal
try/catch block and encountered an exception, a boolean
could not distinguish whether an unwinding event was already underway
when an object was created. Consequently, RAII types like scope guards
could not reliably determine whether their surrounding scope was exiting
normally or via a new exception.
The Thread-Local Exception Counter
Modern C++ runtimes implement std::uncaught_exceptions()
by exposing a thread-local integer counter managed by the exception
handling ABI (such as the Itanium C++ ABI or the Microsoft Visual C++
runtime).
Each thread maintains an execution context containing an exception
runtime structure (for example, __cxa_eh_globals in the
Itanium ABI). Within this structure resides a field typically named
uncaughtExceptions. Because each thread has its own call
stack and independent exception handling state, this counter requires no
cross-thread synchronization, ensuring constant-time \(O(1)\) query overhead.
Lifecycle of the Uncaught Exception Count
The runtime increments and decrements this active count at clearly defined points during exception processing:
- Increment on Throw: When a
throwexpression is evaluated, the runtime allocates an exception object and prepares to unwind the call stack. Immediately prior to initiating stack unwinding, the runtime increments the thread's uncaught exception counter by 1. - Persistence During Stack Unwinding: As the runtime
traverses up the call frame, destroying local objects in reverse order
of construction, the counter remains unchanged. Any query to
std::uncaught_exceptions()during the execution of destructors invoked by this unwinding returns the updated, higher count. - Decrement on Catch Entry: The counter decrements by
1 when control transitions into an active handler. Under standard ABIs,
this occurs immediately upon entering the matching
catchblock (specifically, after the exception declaration in thecatchclause is initialized, such as via the ABI call__cxa_begin_catch). At this stage, the exception transitions from "uncaught" to "caught" (or "handled"), even though the exception object may still exist. - Re-throwing: If a catch block executes a
parameterless
throw;, the exception transitions back to the uncaught state, and the runtime increments the counter once again as unwinding resumes. - Nested Exceptions: If a destructor or local function throws a secondary exception while stack unwinding is already occurring, the runtime allocates the new exception and increments the counter again. The counter now correctly reflects multiple concurrently active, uncaught exceptions on that thread's call stack.
Detecting Active Exceptions in RAII Idioms
std::uncaught_exceptions detects whether a scope is
exiting due to an exception by comparing the counter value across the
lifetime of an object.
An RAII object captures the baseline count at instantiation:
class ScopeGuard {
int initial_exceptions = std::uncaught_exceptions();
public:
~ScopeGuard() {
if (std::uncaught_exceptions() > initial_exceptions) {
// A new exception is currently unwinding this scope
handle_failure();
} else {
// The scope exited normally, or existing unwinding did not originate here
handle_success();
}
}
};If std::uncaught_exceptions() in the destructor
evaluates to a value strictly greater than
initial_exceptions, the runtime is actively unwinding a new
exception thrown after the object was initialized. If the count matches,
the scope completed cleanly, even if the entire operation was nested
inside an outer exception-handling unwinding sequence.