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:
- The runtime automatically invokes
std::terminate(). std::terminate()checks the registeredstd::terminate_handler.- The custom handler executes.
- 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
returnstatement. If it returns, the runtime immediately callsstd::abort(). The handler must explicitly end the process using functions such asstd::abort(),std::_Exit(), orstd::quick_exit(). - The handler must not throw exceptions: If a new
exception escapes the custom handler,
std::terminateis called again recursively, resulting in an immediate call tostd::abort(). Any internal operations (such as rethrowing to identify exception types viastd::current_exception()) must be enclosed within localtry-catchblocks. - 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_terminatemodifies 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.