Exception Specifications and std::terminate
noexcept specifier and operator, when std::terminate fires, custom terminate handlers, stack unwinding guarantees, and cross-thread exception propagation.
noexceptsince C++11The noexcept specifier declares that a function will not propagate exceptions, making this a compile-time-visible contract that the runtime enforces by calling std::terminate if violated.
Overview
C++11 replaced the ill-conceived dynamic exception specifications (throw(T...)) with noexcept. The old syntax required the runtime to check thrown types at each call site and call std::unexpected on mismatch β expensive, rarely useful, and removed entirely in C++17.
noexcept is different in kind: it is a static contract. No runtime type checking occurs. If a noexcept function allows an exception to escape its body, the runtime calls std::terminate immediately. This is deliberate: it enables critical compiler optimisations (especially move operations in containers) and eliminates the overhead of propagation bookkeeping along noexcept call paths.
noexcept is also part of the function type since C++17. Before C++17, two otherwise identical function types differing only in noexcept were the same type; since C++17 they are distinct, and a pointer to a noexcept function is implicitly convertible to a pointer to its non-noexcept counterpart but not vice versa.
Syntax
// noexcept specifier β unconditional
void commit() noexcept; // will not throw
void rollback() noexcept(true); // identical to above
// noexcept(false) β same as no specifier; documents "may throw"
void open() noexcept(false);
// Conditional noexcept β derives guarantee from operations used
template<typename T>
void relocate(T* dst, T* src, std::size_t n)
noexcept(std::is_nothrow_move_constructible_v<T>) // C++11 type trait
{
for (std::size_t i = 0; i < n; ++i)
::new(dst + i) T(std::move(src[i]));
}
// noexcept operator β compile-time predicate, evaluates unevaluated
static_assert( noexcept(commit())); // true
static_assert(!noexcept(open())); // true
// Combine: propagate noexceptness from member operations
template<typename T>
struct Stack {
void push(T val) noexcept(noexcept(data_.push_back(std::move(val))));
private:
std::vector<T> data_;
};
// C++17: noexcept is part of the function type
using Throwing = void();
using NonThrowing = void() noexcept;
static_assert(!std::is_same_v<Throwing, NonThrowing>); // C++17
void (*fp_nt)() noexcept = &commit; // ok
void (*fp_t)() = fp_nt; // ok: noexcept* β plain* implicit
// void (*fp_nt2)() noexcept = fp_t; // error: plain* β noexcept* forbiddenExamples
Conditional noexcept and container reallocation
std::vector uses move construction during reallocation only when the element type's move constructor is noexcept. Without noexcept, it falls back to copy to preserve the strong exception guarantee.
struct Cheap {
Cheap(Cheap&&) noexcept = default; // vector will move during realloc
Cheap& operator=(Cheap&&) noexcept = default;
};
struct Expensive {
Expensive(Expensive&&) = default; // noexcept not declared
// vector will COPY during realloc β pays the copy cost for safety
};
// Check what the container will do:
static_assert(std::is_nothrow_move_constructible_v<Cheap>);
static_assert(!std::is_nothrow_move_constructible_v<Expensive>);Cross-thread exception propagation with std::exception_ptr
std::exception_ptr (C++11) is a nullable, shared-ownership handle to a captured exception. It is the mechanism underlying std::promise/std::future and any manual thread-boundary propagation.
#include <exception>
#include <thread>
#include <vector>
class ParallelTask {
public:
void run() {
thread_ = std::thread([this] {
try {
execute();
} catch (...) {
exception_ = std::current_exception(); // C++11: capture any exception
}
});
}
void join() {
thread_.join();
if (exception_)
std::rethrow_exception(exception_); // re-throws on caller's thread
}
private:
virtual void execute() = 0;
std::thread thread_;
std::exception_ptr exception_; // null if no exception occurred
};Custom terminate handler with exception introspection
#include <exception>
#include <cstdlib>
#include <print> // C++23 β use fprintf for earlier standards
[[noreturn]] void my_terminate() noexcept {
if (auto ep = std::current_exception()) { // non-null during exception-caused terminate
try {
std::rethrow_exception(ep);
} catch (const std::exception& e) {
std::println(stderr, "[terminate] std::exception: {}", e.what());
} catch (...) {
std::println(stderr, "[terminate] unknown exception type");
}
} else {
std::println(stderr, "[terminate] called without active exception");
}
std::abort(); // produces core dump; do not return or throw
}
int main() {
auto* prev = std::set_terminate(my_terminate); // returns previous handler
// std::get_terminate() (C++11) retrieves current handler without replacing it
// ...
}The handler must not throw and must not return. std::abort is the appropriate exit β it generates a core dump on most platforms and triggers SIGABRT handlers.
Best Practices
Mark destructors and move operations noexcept. Both are called in contexts where exceptions cannot be tolerated: destructors during stack unwinding, move constructors during container reallocation. All standard library containers assume moves are noexcept to provide the strong guarantee. Compilers will warn if a special member that should be noexcept is not.
Use conditional noexcept in generic code. Templates that forward to user-supplied types should propagate the user type's noexcept guarantee rather than either always claiming noexcept or always forfeiting it.
template<typename T>
void swap_vals(T& a, T& b)
noexcept(std::is_nothrow_swappable_v<T>) // C++17 type trait
{
using std::swap;
swap(a, b);
}Provide a named close() alongside destructors. Destructors must swallow errors they cannot handle. Give callers a separate function that can throw, so they can handle cleanup failures explicitly. The destructor calls close() silently; callers who care call it themselves.
Install a terminate handler in production binaries. The default handler calls abort() with no diagnostic output. A custom handler that logs the active exception and a stack trace is the difference between a mystery crash and an actionable bug report.
Common Pitfalls
Stack unwinding is not guaranteed after noexcept violation. When a noexcept function allows an exception to escape, the standard permits β but does not require β the runtime to unwind the stack before calling terminate. MSVC typically runs destructors; GCC and Clang often do not. Any cleanup that must happen must happen before the throw site, not in destructors of locals in the noexcept function.
struct Logger {
~Logger() { std::println("flushing log"); } // may not run if noexcept violated
};
void process() noexcept {
Logger log;
throw std::runtime_error("bug"); // terminate; log's dtor: implementation-defined
}Double exception = terminate. If a destructor throws during stack unwinding of another exception, std::terminate is called. There is no way to catch both exceptions. Mark destructors noexcept and swallow errors inside them.
Threads do not propagate exceptions automatically. An exception that escapes a std::thread function calls std::terminate on join (actually at the point of escape β the thread's destructor being called while the thread is joinable also terminates). Use std::exception_ptr explicitly, or prefer std::async / std::jthread (C++20) which handle this more safely.
// C++20: std::jthread with stop_token β still does not propagate exceptions
// Use async for automatic propagation:
auto fut = std::async(std::launch::async, [] {
throw std::runtime_error("from async");
return 42;
});
fut.get(); // rethrows here β exception crosses the thread boundary automaticallynoexcept is not const β it does not prevent internal failures. It is a contract about propagation, not about correctness. A noexcept function may still trigger std::terminate via allocation failure if new throws and there is no handler, or via any of the other terminate conditions listed above.
Removed: dynamic exception specifications (C++17). The throw(T, U) syntax was deprecated in C++11 and removed in C++17. throw() (empty specification) was equivalent to noexcept but slower; migrate all occurrences to noexcept.
void legacy() throw(std::runtime_error); // ERROR in C++17
void legacy_empty() throw(); // ERROR in C++17; use noexceptWindows Structured Exception Handling (SEH)
Windows Structured Exception Handling is a compiler and OS mechanism, separate from and predating C++ exceptions, that intercepts hardware faults and software-raised exceptions at the operating-system level. It is implemented by MSVC via three non-standard keywords: __try, __except, and __finally. Unlike C++ exceptions, which are a pure software construct, SEH exceptions originate in the Windows kernel and can represent hardware faults β null pointer dereferences, divide-by-zero, stack overflow β that have no representation in the C++ type system.
SEH is not C++ and cannot be used interchangeably with try/catch. A __try block cannot be mixed with C++ object construction inside the same function (the compiler rejects it), and SEH by default does not trigger C++ destructors. Understanding SEH matters for systems programmers writing crash reporters, fault-tolerant services, or debuggers on Windows, even when new application code should not use it directly.
__try, __except, and __finally Syntax
A guarded section uses __try paired with either __except (to catch) or __finally (to guarantee cleanup), but not both at once in the same function scope.
// Exception handler form
__try {
int* p = nullptr;
*p = 42; // raises EXCEPTION_ACCESS_VIOLATION (0xC0000005)
}
__except(EXCEPTION_EXECUTE_HANDLER) {
// execution resumes here; the faulting frame has been unwound
printf("caught access violation\n");
}
// Termination handler form β guaranteed to run
__try {
do_work();
}
__finally {
cleanup(); // runs whether do_work() returned normally or raised an exception
}The expression inside __except(...) is the filter expression. It is evaluated in the context of the faulting thread while the faulting frame is still live on the stack (before any unwinding). Three integer return values drive the dispatcher:
| Constant | Value | Effect |
|---|---|---|
EXCEPTION_EXECUTE_HANDLER | 1 | Unwind to this frame and execute the __except body |
EXCEPTION_CONTINUE_SEARCH | 0 | Decline; pass to the next enclosing handler |
EXCEPTION_CONTINUE_EXECUTION | -1 | Resume execution at the faulting instruction |
EXCEPTION_CONTINUE_EXECUTION is almost always wrong. After a null pointer dereference the faulting instruction will simply fault again, producing an infinite loop. Legitimate uses are confined to guard-page schemes where the filter maps new memory before returning.
Filter Helpers: GetExceptionCode and GetExceptionInformation
Two intrinsics are available exclusively inside the filter expression (not inside the __except body itself):
__try {
risky_call();
}
__except(filter_fn(GetExceptionCode(), GetExceptionInformation())) {
// handler body
}
int filter_fn(DWORD code, EXCEPTION_POINTERS* ep) {
if (code == EXCEPTION_ACCESS_VIOLATION) {
// ep->ExceptionRecord->ExceptionAddress β faulting instruction
// ep->ExceptionRecord->ExceptionInformation[0] β 0=read, 1=write, 8=DEP
// ep->ExceptionRecord->ExceptionInformation[1] β faulting address
// ep->ContextRecord β full CPU register state at the fault
log_crash(ep);
return EXCEPTION_EXECUTE_HANDLER;
}
return EXCEPTION_CONTINUE_SEARCH;
}GetExceptionCode() returns the NTSTATUS code for the exception. GetExceptionInformation() returns an EXCEPTION_POINTERS* that contains both the EXCEPTION_RECORD (what faulted) and the CONTEXT (register snapshot). Both intrinsics are only valid during filter evaluation; capturing the EXCEPTION_POINTERS* pointer itself into the __except body is undefined β copy the fields you need in the filter.
Hardware Exceptions as SEH
Windows converts CPU fault signals into SEH exceptions before any user-mode code sees them. Common codes:
| Exception code | Constant | Cause |
|---|---|---|
0xC0000005 | EXCEPTION_ACCESS_VIOLATION | Null deref, unmapped page, write to read-only |
0xC0000094 | EXCEPTION_INT_DIVIDE_BY_ZERO | Integer divide or modulo by zero |
0xC00000FD | EXCEPTION_STACK_OVERFLOW | Stack probe failed (guard page hit) |
0x80000003 | EXCEPTION_BREAKPOINT | __debugbreak() / int 3 instruction |
0x80000004 | EXCEPTION_SINGLE_STEP | Hardware single-step trap |
0xC0000096 | EXCEPTION_PRIV_INSTRUCTION | Privileged instruction in user mode |
Catching and continuing from most of these is wrong. A null pointer dereference means the program's invariants are already broken; resuming execution with corrupted state is worse than terminating. Stack overflow is particularly treacherous: the guard page has been consumed, so even executing the filter expression may trigger another fault. The correct response to EXCEPTION_STACK_OVERFLOW is to log and terminate on a separate thread with sufficient stack.
SEH vs C++ Exceptions: Object Lifetime and Unwinding
This is the most dangerous interaction in the Windows exception model. By default, MSVC compiles with /EHs (synchronous exception handling), which assumes exceptions can only be thrown at explicit throw sites. Under this model, the compiler does not generate unwind tables covering ordinary C++ object destructors for SEH exceptions β a hardware fault inside a __try block that contains C++ objects with destructors will not call those destructors.
// Compiled with /EHs (the default): RESOURCE LEAKED
void dangerous() {
std::unique_ptr<Foo> p = std::make_unique<Foo>(); // destructor registered? NO under /EHs for SEH
__try {
*(int*)nullptr = 0; // access violation
}
__except(EXCEPTION_EXECUTE_HANDLER) {
// p's destructor has NOT been called β Foo is leaked
}
}The /EH compiler flags control this:
| Flag | Name | SEH behaviour |
|---|---|---|
/EHs | Synchronous (default with /EHsc) | C++ destructors only on throw; SEH bypasses them |
/EHa | Asynchronous | C++ destructors called for SEH exceptions too |
/EHsc | /EHs + extern "C" assumed nothrow | Most common production setting |
/EHr | noexcept enforced at runtime (deprecated) | Rarely used |
/EHa makes the program safer when mixing SEH and C++ objects, but at a cost: every function that constructs an object with a destructor must emit unwind metadata, which increases binary size and slightly reduces performance. More importantly, /EHa can mask bugs β an access violation silently destroyed and reconstructed state is harder to diagnose than a crash.
The right mental model: treat __try blocks as C, not C++. Do not construct non-trivial C++ objects inside them. Move all C++ RAII wrappers outside the __try scope.
Converting SEH Exceptions to C++ Exceptions with _set_se_translator
_set_se_translator (declared in <eh.h>) installs a per-thread callback that is invoked during the SEH filter phase. The callback may throw a C++ exception, which then propagates normally through catch blocks:
#include <eh.h>
#include <stdexcept>
#include <windows.h>
class SehException : public std::runtime_error {
public:
explicit SehException(DWORD code)
: std::runtime_error(describe(code)), code_(code) {}
DWORD code() const noexcept { return code_; }
private:
DWORD code_;
static std::string describe(DWORD c) {
switch (c) {
case EXCEPTION_ACCESS_VIOLATION: return "access violation";
case EXCEPTION_INT_DIVIDE_BY_ZERO: return "integer divide by zero";
case EXCEPTION_STACK_OVERFLOW: return "stack overflow";
default: {
char buf[32];
snprintf(buf, sizeof(buf), "SEH 0x%08X", c);
return buf;
}
}
}
};
void se_translator(unsigned int code, EXCEPTION_POINTERS* /*ep*/) {
throw SehException(code);
}
// Must be installed per-thread:
void init_seh_translation() {
_set_se_translator(se_translator); // requires /EHa
}
// Usage:
void example() {
init_seh_translation();
try {
int x = 0;
int y = 1 / x; // raises EXCEPTION_INT_DIVIDE_BY_ZERO
} catch (const SehException& e) {
// caught as a normal C++ exception
fprintf(stderr, "hardware fault: %s (0x%08X)\n", e.what(), e.code());
}
}Requirements and caveats:
_set_se_translatorrequires/EHa. Without it, the translator is called but the thrown C++ exception may bypass destructors.- The translator is per-thread. It must be installed on every thread that needs conversion, including thread-pool threads.
- Catching
SehExceptionand continuing is still usually wrong for hardware faults β the same invariant-violation concern applies. The translator is most useful for crash reporters that want a uniformcatch (...)at the top of the call stack before logging and terminating. - Do not install a translator in a library; it affects the entire thread and will break callers who expect raw SEH.
__finally for Cleanup
__finally provides a guaranteed-execution cleanup block that runs regardless of whether the __try body exits normally or via exception. It is the SEH analogue of RAII, but expressed as explicit code rather than object lifetimes:
HANDLE h = CreateFile(path, GENERIC_READ, 0, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (h == INVALID_HANDLE_VALUE) return;
__try {
process_file(h); // may raise any SEH exception
}
__finally {
CloseHandle(h); // runs in all cases except ExitProcess/TerminateProcess
}__finally does not run if ExitProcess, TerminateProcess, or ExitThread is called, or if the OS kills the process. It also does not run if __except(EXCEPTION_EXECUTE_HANDLER) in an outer frame catches the exception first β the __finally in the inner frame runs during unwind, but only up to the catching frame.
__finally is inferior to RAII for four reasons: it requires manually duplicating the cleanup logic rather than encoding it in a type, it is error-prone when refactoring, it cannot be reused across call sites, and the compiler cannot verify that the invariants are maintained. In modern C++ on Windows, the correct pattern is still RAII β wrap HANDLE, CRITICAL_SECTION, and other OS resources in std::unique_ptr with a custom deleter or in a wrapper class.
When to Use SEH in Modern C++
Almost never. The legitimate remaining use cases are narrow:
Crash reporters. A top-level __try/__except that catches all exceptions, captures GetExceptionInformation(), serialises a minidump via MiniDumpWriteDump, and then re-raises or terminates. This is the primary surviving use in production code and is the pattern used by Chromium, Firefox, and most Windows applications with telemetry.
Fault-tolerant service loops. A Windows service that processes independent requests may use a top-level __try/__except(EXCEPTION_EXECUTE_HANDLER) to isolate one request's crash from the service's continued operation. This is a pragmatic last resort, not a design goal.
Hardware fault simulation and testing tools. Debuggers, profilers, and memory checkers (like the AddressSanitizer runtime on Windows) intercept hardware exceptions deliberately.
None of these justify __try in application logic. If you are reaching for __try to handle a recoverable error condition, the correct answer is to fix the bug that caused the hardware fault, or to use C++ exceptions with a proper error type.
// Wrong: catching access violation to "handle" a null check
__try {
use(*ptr);
}
__except(GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION
? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {
// This hides a null-pointer bug; ptr should have been checked before use
}
// Right:
if (!ptr) throw std::invalid_argument("ptr must not be null");
use(*ptr);The performance argument for SEH over C++ exceptions is also largely obsolete. MSVC's C++ exception implementation uses the same zero-cost table-driven unwind mechanism that SEH does; thrown exceptions are not faster under SEH than under throw. The only overhead difference is in _set_se_translator scenarios, and that overhead is dwarfed by the cost of actual fault processing.
See Also
reference/language/exceptionsβ exception hierarchy, try/catch mechanics, exception safety guaranteesreference/language/raiiβ RAII as the mechanism that makes stack unwinding usefulreference/language/move-semanticsβ whynoexceptmove constructors matter for container performancereference/language/coroutinesβco_awaitand exception propagation through coroutine frames