C++ Exception Specifications: noexcept, History, and throw

Key takeaways

C++ exception specifications from throw() to noexcept: move operations, swap, destructors, conditional noexcept, and why dynamic specs were removed.

Introduction

Exception specifications describe what exceptions a function may throw, and — in the case of noexcept — whether a function is allowed to throw at all. This sounds like a small piece of syntax, but it has real consequences for the code generated by the compiler, for the exception-safety guarantees a class can offer, and for the performance of code that moves objects around, such as std::vector reallocations. Understanding exception specifications matters because the wrong choice in either direction — marking something noexcept that can actually throw, or leaving something un-marked that never throws — produces bugs and missed optimizations that are hard to spot in review.

This post walks through the history of exception specifications in C++, why the old throw() syntax was deprecated and eventually removed, how noexcept works both as a specifier and as a compile-time boolean operator, and the specific places in a class design — move constructors, move assignment, swap, destructors — where getting noexcept right or wrong has outsized consequences.

History of exception specifications

C++03 — throw()

Before C++11, the language offered dynamic exception specifications. You could write throw() to claim a function throws nothing, or throw(std::exception) to claim it throws only that type (or subtypes of it).

// No exceptions
void func() throw();
// Specific types
void func() throw(std::exception);
void func() throw(int, double);
// Removed in C++17

The problem was enforcement. If a function declared throw(std::exception) but actually threw something else, the runtime didn’t reject the throw at the call site — it called std::unexpected(), which by default calls std::terminate(). Worse, this was a runtime check, not a compile-time one, so violations only surfaced when the offending code path actually executed, often in production rather than in a unit test. The type-list form (throw(int, double)) also interacted badly with templates and inheritance: a derived class overriding a virtual function had to respect a compatible (narrower or equal) exception specification, which made generic code and library evolution painful. Because of these problems, dynamic exception specifications with a type list were deprecated in C++11 and removed entirely in C++17 — only the exceptionless case survives, and even that was reframed as syntactic sugar for noexcept(true) rather than a genuinely separate mechanism.

C++11 — noexcept

C++11 replaced this with noexcept, which comes in two forms: a bare specifier meaning “never throws,” and a conditional form that takes a compile-time boolean expression.

// No exceptions
void func() noexcept;
// Conditional
void func() noexcept(true);   // noexcept
void func() noexcept(false);  // may throw

The key design difference from throw() is that noexcept doesn’t try to describe which exceptions might propagate — it’s a binary contract: either the function is guaranteed not to throw, or it isn’t. There’s no partial specification of allowed exception types. This simplification is deliberate: the committee recognized that fine-grained exception type lists were rarely useful and frequently wrong, whereas a simple boolean is something the compiler and the standard library can actually optimize around.

noexcept basics

Basic usage

#include <iostream>
void safe_func() noexcept {
    std::cout << "no throw" << std::endl;
}
void risky_func() {
    throw std::runtime_error("error");
}
int main() {
    safe_func();

    try {
        risky_func();
    } catch (const std::exception& e) {
        std::cout << e.what() << std::endl;
    }

    return 0;
}

Here safe_func makes a promise to the compiler and to every caller: it will not let an exception escape. risky_func makes no such promise, so ordinary try/catch handling around it works as expected. Note that the compiler does not verify at compile time that safe_func’s body is actually exception-free — it can call throwing functions internally, and if one of them throws, the program terminates rather than propagating the exception. noexcept is a promise you make to the compiler, not a guarantee the compiler proves for you. This asymmetry — “trust me” semantics with catastrophic failure if you’re wrong — is the single most important thing to internalize about this feature.

Checking noexcept

noexcept is overloaded syntactically: the same keyword is also a compile-time operator that evaluates whether an expression is known not to throw, returning a bool usable in constant expressions.

#include <iostream>
#include <type_traits>
void func1() noexcept {}
void func2() {}
int main() {
    std::cout << std::boolalpha;
    std::cout << "func1: " << noexcept(func1()) << std::endl;  // true
    std::cout << "func2: " << noexcept(func2()) << std::endl;  // false

    return 0;
}

This dual role — noexcept as a specifier attached to a function declaration, and noexcept(expr) as an operator that inspects an expression — trips up a lot of people reading unfamiliar code for the first time. The operator form does not evaluate its operand at runtime; it only inspects the operand’s static type information (specifically, whether every function call inside the expression is itself marked noexcept, transitively). This is what makes the noexcept(noexcept(...)) idiom possible, covered below — you can ask “is this specific expression, involving this specific template parameter, provably non-throwing?” and get an answer during template instantiation, before any code runs.

Move operations and noexcept

Move constructor

#include <vector>
#include <iostream>
class Widget {
public:
    Widget() = default;

    Widget(Widget&& other) noexcept
        : data(std::move(other.data)) {
        std::cout << "move ctor" << std::endl;
    }

    Widget(const Widget& other)
        : data(other.data) {
        std::cout << "copy ctor" << std::endl;
    }

private:
    std::vector<int> data{1, 2, 3};
};
int main() {
    std::vector<Widget> vec;
    vec.reserve(10);

    Widget w;
    vec.push_back(std::move(w));  // move ctor (noexcept)

    return 0;
}

Important: std::vector often requires noexcept move constructors to move elements during reallocation.

The reason is subtle but important: std::vector offers the strong exception-safety guarantee during reallocation — if growing the vector fails partway through, the vector must be left exactly as it was before the call, with no partial state visible to the caller. If Widget’s move constructor could throw, and it threw halfway through moving the existing elements into the new buffer, vector would have no safe way to roll back: some elements have already been moved-from (and are now in an unspecified but valid state), and the new buffer holds a partial copy. To preserve the strong guarantee under these conditions, vector falls back to copying elements during reallocation instead of moving them whenever the move constructor is not marked noexcept. This is checked via std::is_nothrow_move_constructible, which the standard library consults internally through std::move_if_noexcept. The practical upshot: forgetting noexcept on a move constructor doesn’t just fail to gain a performance optimization — for types with non-trivial copies, it silently downgrades every vector::push_back-triggered reallocation from an O(n) move to an O(n) copy, and nobody gets a compiler warning about it.

swap and noexcept

swap implementation

class Data {
public:
    Data(int v) : value(v) {}

    void swap(Data& other) noexcept {
        std::swap(value, other.value);
    }

    int getValue() const noexcept { return value; }

private:
    int value;
};
namespace std {
    template<>
    void swap(Data& lhs, Data& rhs) noexcept {
        lhs.swap(rhs);
    }
}
int main() {
    Data d1(10), d2(20);
    std::swap(d1, d2);

    std::cout << d1.getValue() << std::endl;  // 20
    std::cout << d2.getValue() << std::endl;  // 10
    return 0;
}

swap marked noexcept is foundational to the copy-and-swap idiom used to implement assignment operators with the strong exception-safety guarantee: you build a temporary copy (which may throw, but at that point nothing has been mutated yet), and then swap the temporary into place. If the swap step itself could throw, you’d be back to square one — a partially-completed assignment with no way to recover. That’s why std::swap for well-designed value types, and any hand-written swap member or free function, should be noexcept whenever the underlying operation genuinely can’t fail — which for a pointer or scalar-only swap is almost always true, since it reduces to a handful of trivial assignments.

Conditional noexcept

Templates and noexcept

template<typename T>
class Container {
public:
    Container(Container&& other)
        noexcept(std::is_nothrow_move_constructible_v<T>)
        : data(std::move(other.data)) {}

    void clear() noexcept(std::is_nothrow_destructible_v<T>) {
        data.clear();
    }

private:
    std::vector<T> data;
};

Generic code can’t simply hardcode noexcept on operations that depend on a template parameter’s behavior, because whether the operation actually throws depends entirely on what T is. Container<int>’s move constructor should be noexcept; Container<SomeThrowingType>’s should not claim to be. Conditional noexcept expressions solve this by deferring the decision to a trait evaluated at instantiation time, so the exception specification is correct for every instantiation rather than being a lie for some of them.

This is also where the noexcept(noexcept(...)) idiom comes from — you’ll see this pattern extensively in the standard library’s own headers:

template<typename T>
void process(T&& value) noexcept(noexcept(value.doWork())) {
    value.doWork();
}

The inner noexcept(value.doWork()) is the operator, asking “is calling doWork() on this expression known not to throw?” The outer noexcept(...) is the specifier, taking that boolean answer and applying it to process itself. Read inside-out: evaluate whether the call can throw, then propagate that answer as this function’s own contract. It looks redundant until you understand that the two noexcept tokens are doing genuinely different jobs — one queries, one declares.

Destructors and noexcept

Implicit noexcept

class MyClass {
public:
    // destructor implicitly noexcept unless a member's dtor can throw
    ~MyClass() {
        // throwing here -> std::terminate during stack unwind
    }
};
class Explicit {
public:
    ~Explicit() noexcept {
        // still must not throw
    }
};

Destructors are implicitly noexcept by default in C++11 and later, unless a base class or member has a destructor that is explicitly marked noexcept(false). This default exists for a hard reason: destructors run during stack unwinding while another exception is already propagating. If a second exception escapes a destructor while the first is still in flight, there is no sane way to have two live exceptions at once, so the standard mandates std::terminate(). This is precisely why the guidance “never let a destructor throw” isn’t merely a style preference — it’s backed by a language rule with an unrecoverable failure mode if violated.

Exceptions in destructor cleanup

class SafeClass {
public:
    ~SafeClass() noexcept {
        try {
            cleanup();
        } catch (const std::exception& e) {
            std::cerr << "cleanup error: " << e.what() << std::endl;
        }
    }

private:
    void cleanup() {
        // may throw
    }
};

If a destructor calls something that might throw, the only sound options are to catch and swallow (or log) the exception inside the destructor, or to ensure the throwing path genuinely can’t occur by the time the destructor runs. Catching ... (catch-all) at the top level of a destructor is a defensible last line of defense specifically because the alternative — letting anything escape — is std::terminate(), not a caught exception somewhere up the call stack. This is one of the few places in C++ where “catch and do nothing useful with it beyond logging” is legitimate defensive coding rather than a code smell.

A production incident: std::terminate from a “safe” move

I once tracked down a crash that only happened under load, never in the debugger, and never in the small unit tests that exercised the class in isolation. The class had a move constructor marked noexcept, and it moved a std::vector member plus a small owned buffer allocated with a custom allocator. What I’d missed was that the custom allocator’s move path, several layers down, called into a logging subsystem that under specific memory-pressure conditions could itself throw std::bad_alloc while trying to allocate a log buffer. That exception surfaced from inside a function chain that was, at the top, promised noexcept. The result was an immediate std::terminate() — no stack trace pointing at the real cause, just an abort deep in the C++ runtime, because by the time terminate runs, the original exception’s context is largely gone. It only reproduced under load because the memory-pressure condition that made the allocator’s internal throw path reachable only occurred when the process was already under heavy allocation churn.

The lesson that stuck with me: noexcept is a promise about the entire call graph beneath a function, not just the code you can see in its body. Marking something noexcept because it “looks like” it can’t throw, without tracing every call it makes — including calls several layers into a library you don’t own — is how these crashes happen. After that incident, I got much more conservative about applying noexcept to anything that touches allocation, logging, or third-party code whose exception behavior isn’t documented, and I started grepping vendor headers for noexcept annotations before assuming a call chain was safe. A related but subtler bug I’ve seen in review: a type whose move constructor is marked noexcept but whose move assignment operator isn’t, because someone updated one and forgot the other — std::vector and friends check each operation independently, so an inconsistency like that quietly reintroduces the copy fallback for one operation while the other stays fast, and nothing in a compile warns you about the asymmetry.

Practical example: resource management

#include <iostream>
#include <memory>
class Resource {
public:
    Resource() {
        std::cout << "allocate" << std::endl;
    }

    ~Resource() noexcept {
        std::cout << "release" << std::endl;
    }

    void use() noexcept {
        std::cout << "use" << std::endl;
    }
};
class Manager {
public:
    Manager() : res(std::make_unique<Resource>()) {}

    Manager(Manager&& other) noexcept
        : res(std::move(other.res)) {}

    Manager& operator=(Manager&& other) noexcept {
        res = std::move(other.res);
        return *this;
    }

    void process() noexcept {
        if (res) {
            res->use();
        }
    }

private:
    std::unique_ptr<Resource> res;
};
int main() {
    Manager m1;
    m1.process();

    Manager m2 = std::move(m1);
    m2.process();

    return 0;
}

Manager is a reasonable model of the pattern you want for a RAII wrapper: the move constructor and move assignment operator move a std::unique_ptr, an operation that is itself noexcept (moving a unique_ptr is just a pointer swap plus a null-out, never an allocation), so it’s safe and correct to propagate that guarantee. The destructor releasing the underlying resource is likewise noexcept by the implicit default. This is the shape most hand-written resource-owning classes should aim for: cheap, non-throwing move operations built entirely out of other non-throwing operations, so the noexcept markings are true by construction rather than by hope.

Where to put noexcept

Functionnoexcept?Reason
Move ctorYesContainer performance (avoids copy fallback)
Move assignYesContainer performance; must match move ctor
swapYesEnables copy-and-swap’s strong exception safety
DestructorYes (default)Avoid terminate during unwind
Simple gettersOftenNo throws
Complex logic / allocation / third-party callsVerify before markingMay throw indirectly through a call chain you don’t fully control

noexcept is a promise the compiler does not check. If an exception escapes a noexcept function, the program calls std::terminate, and the stack is not guaranteed to be unwound, so destructors of local objects may never run. A wrong noexcept therefore turns a recoverable error into a crash, which is worse than leaving the specifier off.

For move operations, prefer letting the compiler decide: a defaulted move constructor is noexcept exactly when all members’ move constructors are. Then assert what you rely on, for example static_assert(std::is_nothrow_move_constructible_v<Widget>); next to the class, so a later member change that silently makes std::vector<Widget> fall back to copying fails the build instead.