Skip to content
C++
Language
since C++11
Intermediate

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++11

The 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

cpp
// 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* forbidden

Examples

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.

cpp
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.

cpp
#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

cpp
#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.

cpp
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.

cpp
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.

cpp
// 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 automatically

noexcept 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.

cpp
void legacy() throw(std::runtime_error);  // ERROR in C++17
void legacy_empty() throw();              // ERROR in C++17; use noexcept

Windows 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.

cpp
// 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:

ConstantValueEffect
EXCEPTION_EXECUTE_HANDLER1Unwind to this frame and execute the __except body
EXCEPTION_CONTINUE_SEARCH0Decline; pass to the next enclosing handler
EXCEPTION_CONTINUE_EXECUTION-1Resume 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):

cpp
__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 codeConstantCause
0xC0000005EXCEPTION_ACCESS_VIOLATIONNull deref, unmapped page, write to read-only
0xC0000094EXCEPTION_INT_DIVIDE_BY_ZEROInteger divide or modulo by zero
0xC00000FDEXCEPTION_STACK_OVERFLOWStack probe failed (guard page hit)
0x80000003EXCEPTION_BREAKPOINT__debugbreak() / int 3 instruction
0x80000004EXCEPTION_SINGLE_STEPHardware single-step trap
0xC0000096EXCEPTION_PRIV_INSTRUCTIONPrivileged 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.

cpp
// 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:

FlagNameSEH behaviour
/EHsSynchronous (default with /EHsc)C++ destructors only on throw; SEH bypasses them
/EHaAsynchronousC++ destructors called for SEH exceptions too
/EHsc/EHs + extern "C" assumed nothrowMost common production setting
/EHrnoexcept 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:

cpp
#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_translator requires /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 SehException and 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 uniform catch (...) 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:

cpp
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.

cpp
// 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 guarantees
  • reference/language/raii β€” RAII as the mechanism that makes stack unwinding useful
  • reference/language/move-semantics β€” why noexcept move constructors matter for container performance
  • reference/language/coroutines β€” co_await and exception propagation through coroutine frames