How Stack Unwinding Works in C++ Exceptions
Stack unwinding is the runtime process in C++ that cleans up the call
stack when an exception is thrown. As an active exception propagates up
the call chain looking for an appropriate catch block, the
runtime systematically exits active function scopes and invokes the
destructors of all local, automatic objects in reverse order of their
construction. This article explains the exact step-by-step mechanics of
stack unwinding, its interaction with object lifetimes, and the critical
rules governing exception safety.
The Trigger and the Search for a Handler
The unwinding process begins immediately when a throw
expression executes. The C++ runtime halts normal sequential execution
within the current function and creates an exception object in a
dedicated storage area managed by the runtime library.
The runtime then inspects the current function frame for an enclosing
try block equipped with a catch clause
matching the type of the thrown object (or a compatible base class or
void* conversion). If the current frame cannot handle the
exception, the runtime must exit the frame and examine the calling
function.
Destruction of Local Objects
Before the runtime can pop a stack frame and pass control to the caller, it must maintain C++ object lifetime guarantees. It performs cleanup through the following steps:
- Reverse-Order Destruction: Every fully constructed automatic object in the exiting scope is destroyed. Destructors are called in the exact reverse order of the objects' constructors.
- Block Scope Traversal: If a function contains
nested blocks (
{ ... }), unwinding proceeds outwards, destroying objects in inner scopes before destroying those in outer scopes. - Sub-Object Cleanup: If an exception is thrown inside a constructor, stack unwinding ensures that any base classes and member variables that were already fully initialized are destroyed in reverse order. The destructor of the class whose constructor failed is never called.
This mechanism forms the backbone of the Resource Acquisition Is
Initialization (RAII) idiom. Smart pointers (like
std::unique_ptr), file handles, and mutex locks
automatically release their managed resources during this phase without
requiring explicit cleanup code.
Transfer of Control to the Catch Block
The runtime repeats this inspection and cleanup loop up the call
hierarchy—from callee to caller—until it identifies a stack frame
containing a try block with a matching catch
handler:
- Handler Found: The runtime destroys remaining local
objects in that frame up to the point of the
tryblock, initializes the exception declaration in thecatchclause (if any), and transfers control to the catch block body. The exception is now considered caught, and normal execution resumes. - No Handler Found: If the runtime reaches the entry
point of the program (such as
main()) or a thread boundary without encountering a matching handler, it callsstd::terminate(). In this scenario, whether remaining stack frames are unwound is implementation-defined.
Destructor Failures and
std::terminate
A critical rule of stack unwinding is that a destructor must not allow an exception to escape during an active unwind.
If a destructor throws an exception while the stack is already
unwinding due to an earlier, unhandled exception, the C++ runtime cannot
safely manage two concurrently active exceptions propagating through the
same thread. Consequently, the runtime immediately calls
std::terminate(), aborting the program without further
cleanup. For this reason, destructors in C++11 and later are implicitly
marked noexcept.