Using std::set_terminate for C++ Unhandled Exceptions

In C++, an unhandled exception bypasses standard control flow and forces the runtime to call std::terminate, leading to an abrupt program exit. The standard library provides std::set_terminate to allow developers to intercept this process by registering a custom callback function. This article explains how std::set_terminate installs a user-defined termination handler, the conditions under which the handler is invoked, and the specific rules and best practices required to log diagnostic data before the process safely halts.

The Role of std::set_terminate

Defined in the <exception> header, std::set_terminate alters the default runtime behavior when the C++ exception-handling mechanism fails. By default, an uncaught exception triggers std::terminate, which typically invokes std::abort. By registering a custom handler, you can execute final diagnostic logging, generate a stack trace, or write a crash dump before the operating system tears down the process.

The function signature is defined as:

std::terminate_handler set_terminate(std::terminate_handler f) noexcept;

std::terminate_handler is a function pointer type representing a function that takes no parameters and returns void:

typedef void (*terminate_handler)();

When called, std::set_terminate installs f as the new global termination handler and returns the previously registered handler, allowing you to chain handlers or restore the old one later if necessary.

How Registration and Execution Work

Registering a handler involves passing a compatible function pointer to std::set_terminate, typically at the very beginning of the program's lifecycle in main().

#include <iostream>
#include <exception>
#include <cstdlib>

void custom_terminate_handler() {
    std::cerr << "Fatal error: Unhandled exception encountered. Aborting.\n";
    
    // Inspect the active exception if one exists
    if (auto ex = std::current_exception()) {
        try {
            std::rethrow_exception(ex);
        } catch (const std::exception& e) {
            std::cerr << "Exception details: " << e.what() << '\n';
        } catch (...) {
            std::cerr << "Exception details: Unknown exception type.\n";
        }
    }
    
    std::abort();
}

int main() {
    std::set_terminate(custom_terminate_handler);

    // Throwing an exception that is not caught
    throw std::runtime_error("Database connection failed");

    return 0;
}

When an exception is thrown and finds no matching catch block during stack unwinding:

  1. The runtime automatically invokes std::terminate().
  2. std::terminate() checks the registered std::terminate_handler.
  3. The custom handler executes.
  4. Using std::current_exception(), the handler can inspect the unhandled exception that initiated termination, even though standard catching failed.

Critical Rules for Custom Handlers

When implementing a custom termination handler, specific language rules must be followed to avoid undefined behavior:

  • The handler must not return: A custom termination handler cannot use a normal return statement. If it returns, the runtime immediately calls std::abort(). The handler must explicitly end the process using functions such as std::abort(), std::_Exit(), or std::quick_exit().
  • The handler must not throw exceptions: If a new exception escapes the custom handler, std::terminate is called again recursively, resulting in an immediate call to std::abort(). Any internal operations (such as rethrowing to identify exception types via std::current_exception()) must be enclosed within local try-catch blocks.
  • Stack unwinding is not guaranteed: When an unhandled exception occurs, the implementation is not required to unwind the stack before invoking std::terminate. Consequently, local destructors in calling frames may not execute. Relying on RAII cleanups inside a custom terminate handler is unsafe.
  • Thread safety: std::set_terminate modifies global state. Changing the handler concurrently from multiple threads without synchronization creates a data race. It is best practice to configure the handler once on the main thread before starting background threads.