Exception Thrown During Stack Unwinding in C++
In C++, throwing an exception while a previous exception is actively
unwinding the stack causes the runtime to immediately abort the program
by calling std::terminate(). This article explains the
mechanics of stack unwinding, why the C++ runtime strictly prohibits
multiple active uncaught exceptions, the impact of modern
noexcept defaults on destructors, and best practices to
prevent unexpected program termination.
The Mechanics of Stack Unwinding
When an exception is thrown, the C++ runtime searches up the call
stack for an appropriate catch block. As execution exits
nested scopes during this search, the runtime automatically invokes the
destructors of all local and automatic objects allocated within those
scopes in reverse order of their construction. This process is known as
stack unwinding.
Stack unwinding guarantees that resources managed via RAII (Resource Acquisition Is Initialization)—such as smart pointers, file handles, and mutex locks—are deterministically freed even when an error occurs.
What Happens When a Second Exception Escapes
During stack unwinding, destructors are actively executing. If code inside a destructor throws a secondary exception, that exception must be caught and handled entirely within the destructor itself.
If the secondary exception is not caught and attempts to propagate
out of the destructor, the runtime faces an impossible dilemma: it has
two distinct, concurrently active exceptions seeking two potentially
different catch handlers. Because the C++ language
specification does not support multiple simultaneous active exception
flows, the runtime immediately calls std::terminate().
When std::terminate() is invoked:
- The program halts execution abruptly.
- No further destructors are called.
- Stack unwinding ceases entirely.
- By default,
std::abort()is executed, generating a core dump or crash log.
Modern C++ and
Implicit noexcept Destructors
Starting with C++11, destructors are implicitly marked as
noexcept(true) unless explicitly declared otherwise or if a
member subobject has a potentially throwing destructor.
Because of this rule, throwing an exception from a standard
destructor triggers std::terminate() in two distinct
ways:
- The Dual-Exception Rule: If stack unwinding is
already underway and a destructor throws an uncaught exception,
std::terminate()is called because of the active unwinding state. - The
noexceptViolation: Even if stack unwinding is not active (e.g., an object simply goes out of scope normally), allowing an uncaught exception to escape anoexcept(true)destructor immediately callsstd::terminate().
Declaring a destructor noexcept(false) disables the
second rule, but it does not bypass the first. If unwinding is active,
escaping an exception from a noexcept(false) destructor
will still invoke std::terminate().
How to Prevent Termination
To maintain program stability, follow these guidelines when writing destructors and cleanup routines:
- Swallow Exceptions in Destructors: Always enclose
potentially throwing operations within a
try-catch(...)block inside destructors, logging or silently handling any errors locally. - Separate Cleanup from Error Reporting: For
operations where failure must be reported (such as closing a file or
flushing a socket network buffer), provide an explicit public method
like
close()orflush()that can throw exceptions, and use the destructor purely as a fallback to release raw resources without throwing. - Query Active Exceptions via
std::uncaught_exceptions(): Introduced in C++17,std::uncaught_exceptions()returns anintrepresenting the current count of active, uncaught exceptions. This allows an object to determine whether it is being destroyed as part of stack unwinding and adapt its behavior accordingly.