How std::bad_alloc Notifies Heap Exhaustion in C++

This article explains how the C++ runtime uses the std::bad_alloc exception to signal heap allocation failures to calling contexts. When system memory is exhausted during dynamic allocation, C++ bypasses traditional error-return codes in favor of an exception-driven mechanism that initiates stack unwinding. Readers will learn the internal process triggered by operator new, the role of std::new_handler, how stack unwinding ensures resource cleanup, and how calling functions can effectively intercept and handle this failure.

The Trigger: Dynamic Memory Allocation Failure

In C++, dynamic memory is predominantly requested using the new expression. Under the hood, this invokes ::operator new to allocate raw bytes from the free store (heap). If the operating system or runtime memory manager cannot fulfill the requested byte size, allocation fails.

Before immediately throwing an exception, the runtime checks for a registered std::new_handler function. If one exists, it is called in an attempt to free memory. If no handler is registered, or if the handler fails to free memory and returns, ::operator new halts execution of the constructor that would have followed and instantiates a std::bad_alloc object, which is defined in the <new> header and derives from std::exception.

Notification via Stack Unwinding

Unlike C's malloc(), which notifies callers by returning a nullptr, standard C++ new uses structured exception handling. Once std::bad_alloc is thrown, normal sequential execution halts immediately.

The C++ runtime initiates stack unwinding to notify the calling context:

  1. Scope Traversal: The runtime searches backward up the call chain, leaving the current scope and evaluating outer scopes for an active try...catch block.
  2. Deterministic Cleanup (RAII): As the runtime exits each stack frame during unwinding, it calls the destructors for all fully constructed local objects in reverse order of their creation. This guarantees that stack-allocated resources (such as file handles, locks, and other smart pointers) are safely released despite the out-of-memory condition.
  3. Target Match: The unwinding continues until it finds a catch clause matching std::bad_alloc or one of its base classes, such as std::exception.

Interception in Calling Contexts

Calling contexts intercept the heap exhaustion notification by wrapping allocation sites in a try block and defining an appropriate handler:

#include <iostream>
#include <new>

void allocateMemory() {
    // Attempting to allocate an impossibly large block
    size_t hugeSize = -1; 
    int* ptr = new int[hugeSize];
    delete[] ptr;
}

int main() {
    try {
        allocateMemory();
    } catch (const std::bad_alloc& e) {
        std::cerr << "Allocation failed: " << e.what() << '\n';
        // Handle failure or gracefully degrade functionality
    }
    return 0;
}

If the calling context catches const std::bad_alloc&, it receives direct notification that memory ran out, allowing the program to log the error, release non-essential caches, or terminate cleanly.

Uncaught Exceptions and Termination

If no calling context up the entire call hierarchy catches the std::bad_alloc exception, the stack unrolls to the top level (typically main()). If left uncaught, the runtime calls std::terminate(). By default, std::terminate invokes std::abort(), immediately ending process execution without completing further resource destruction, thereby protecting system stability from corrupted or incomplete program states.