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:

  1. 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.
  2. The noexcept Violation: Even if stack unwinding is not active (e.g., an object simply goes out of scope normally), allowing an uncaught exception to escape a noexcept(true) destructor immediately calls std::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() or flush() 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 an int representing 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.