C++ has had two different mechanisms for declaring “this function won’t throw” — the old throw(...) exception specification and the modern noexcept specifier — and they enforce that promise in radically different ways. The old mechanism checked at runtime: if a forbidden exception escaped, the program detoured through a runtime handler before terminating. The new mechanism treats noexcept as a compile-time-queryable property, lets templates branch on it for optimisation, and reduces the runtime machinery to a direct call to std::terminate. Understanding why the language switched, and what each approach actually guarantees, is the difference between writing noexcept as decoration and writing it where the compiler genuinely uses it.
Three things to take away:
- C++03’s
throw(types)specifications were checked at runtime — violations calledunexpected()and typically ended instd::terminate(), but the cost was paid at every call. - C++11’s
noexceptis a binary, compile-time-queryable property: the compiler and templates can ask “is this function noexcept?” and generate different code based on the answer. - Mark a function
noexceptonly when its body genuinely cannot throw — a violation still callsstd::terminateat runtime, with no opportunity for recovery.
The old model: throw(...) checked at runtime
Pre-C++11 exception specifications listed the types a function was allowed to throw:
void compute() throw(std::runtime_error);
void serialize() throw(); // throws nothing
void general(); // can throw anything
The standard reference is unambiguous about the enforcement: “the correctness of the exception specification is checked at runtime, not at compile time.” If compute() let an exception of an unlisted type escape, the runtime called unexpected(), whose default behaviour was to call std::terminate().
This had three problems severe enough that C++11 deprecated the mechanism and C++17 removed it entirely:
- Cost at every call. A function declared
throw(X)couldn’t simply propagate exceptions up the stack — the runtime had to intercept anything that escaped and check it against the type list. The check was code the compiler had to generate, around every body with a non-empty spec. - Composability failure. Calling a function with a different spec didn’t compose cleanly. If
f()was declaredthrow(A)and calledg()declaredthrow(B), the compiler couldn’t prove thatf()honoured its own spec without expensive analysis, so it simply emitted the runtime check. - The compiler couldn’t help. A spec mismatch was discovered when an exception actually flew, often at a customer site, long after the code was written. The contract existed in the declaration; the verification existed only at the moment of violation.
The C++03 rule was unusual among static languages. Java’s throws clause is checked at compile time — the compiler refuses to build code that calls a throws IOException method without either handling the exception or declaring the same throws. C++03 chose runtime checking instead, paid the cost, and got little safety in return.
The new model: noexcept is a compile-time property
C++11 replaced the typed list with a single Boolean:
void compute() noexcept; // promises: never throws
void general(); // no promise: might throw
void conditional() noexcept(sizeof(int) == 4); // computed at compile time
noexcept is binary: a function either promises not to throw (noexcept or noexcept(true)) or makes no such promise. The type list is gone — and with it the questions “what about exceptions derived from listed types?” and “what if the handler throws something else?” Either the function commits to throwing nothing, or it doesn’t.
The decisive change is what the compiler does with the information. noexcept is part of the function’s type, and the language exposes it as a compile-time operator:
constexpr bool safe = noexcept(compute()); // true at compile time
static_assert(noexcept(compute()), "compute must not throw");
The expression noexcept(f()) evaluates at compile time to true if every function called within evaluates to noexcept, and false otherwise. This is what “compile-time exception checking” actually means in modern C++: not that violations are caught at compile time (they aren’t — see below), but that the promise is queryable at compile time, and other code can branch on the answer.
What templates do with the answer
The most consequential use of compile-time noexcept-querying is inside the standard library. std::vector, when it has to relocate elements on a reserve or push_back that exceeds capacity, faces a choice: move the elements (fast) or copy them (slow, but safe if a move would throw).
The library uses noexcept to make the choice:
// Conceptual outline of the std::vector reallocation rule:
if constexpr (std::is_nothrow_move_constructible_v<T>){
// Move elements: cheap and exception-safe because moves don't throw
} else {
// Copy elements: more expensive, but if a copy throws partway,
// the original buffer is still intact.
}
The trait std::is_nothrow_move_constructible_v<T> is exactly “is T‘s move constructor declared noexcept?”. A type whose move constructor is noexcept gets the fast path; one whose move might throw gets the slow path. The marker is what unlocks the optimisation. This is the practical reason RAII wrappers (Nibble #071) declare their move constructors noexcept — without it, every std::vector<Wrapper> would copy on growth instead of moving.
Other places noexcept-querying matters: std::move_if_noexcept in the standard library, the strong-vs-basic exception guarantee analysis in container operations, and user-written generic code that can take a faster path when its arguments promise not to throw.
The runtime side: what happens if noexcept is violated
noexcept is a compile-time-queryable property, but the enforcement of the promise is still a runtime affair — and the runtime affair is brutal. If an exception escapes a noexcept function, the runtime calls std::terminate() immediately. There is no unexpected() handler to install, no type-list filtering, no opportunity to convert the exception into a different one. The program ends.
void promised() noexcept{
throw std::runtime_error{"oops"}; // calls std::terminate immediately
}
This is not a bug in the design — it is the design. By giving up the chance to recover, noexcept lets the compiler omit the unwinding tables and exception-frame setup that ordinary functions need. The cost saved is real; the price is that a violation is non-recoverable. Mark a function noexcept only when it genuinely cannot throw.
The compiler does not, in general, verify that the body honours the promise. It will warn in obvious cases (a literal throw expression in the body), but a noexcept function that calls a non-noexcept function and lets exceptions through compiles without complaint. The error surfaces only at runtime, when an exception actually escapes.
Where to put noexcept
A small set of functions almost always benefits from a noexcept declaration:
- Move constructors and move assignment operators — for the
std::vectoroptimisation above. This is the highest-leverage case. swap— likewise, used by the standard library for exception-safe operations.- Destructors — already implicitly
noexceptsince C++11, unless you actively opt out. - Simple accessors —
size(),empty(),data()— that manifestly cannot throw.
A larger set should not be noexcept:
- Anything that allocates memory and can fail (most things that call
newor push to a container). - Anything that calls user-supplied code through a callback, unless the callback is itself constrained.
- Functions where “might throw” is part of the contract — parsers, validators, network operations.
The temptation when first learning noexcept is to put it everywhere; the temptation should be resisted. A wrong noexcept annotation turns a recoverable error into a program abort. A missing one merely loses an optimisation.
Takeaway
The shift from C++03’s throw(types) to C++11’s noexcept moved exception-specification machinery in two directions at once: the promise moved from a typed list to a single Boolean and became compile-time-queryable; the enforcement on violation became more brutal — a direct call to std::terminate with no recovery handler. The combination is what makes the modern feature useful: templates and the standard library can ask “does this function promise not to throw?” at compile time and generate different code based on the answer, without the old runtime overhead.
The rule is simple: write noexcept only where the body genuinely cannot throw, and the compiler — and every template that queries the promise — will use it.