C++ Part I · Module 22 · Error Handling and Diagnostics
noexcept and Where an Exception Must Not Escape
Three chapters have now rested on the same promise. Chapter 22.3's unwinding requires that destructors do not throw, Chapter 22.4's guards require the same, and Chapter 22.5's strong guarantee requires a commit step that cannot fail. noexcept is how that promise is written down, and the frozen purpose asks for three things: knowing where to make it, knowing what the compiler does with it, and knowing why a destructor that throws ends the process. The first and third are straightforward and important. The second is where this chapter found something that contradicts what is usually said about the keyword. The common claim is that noexcept lets the compiler omit exception-handling machinery from the function, making it smaller and faster. Measured on this compiler, the noexcept function is not smaller — it is very slightly larger, and it requires runtime support that the throwing version does not, because the promise has to be enforced: something must catch an escaping exception and call terminate. The benefit is real, but it is in a different place entirely, and the chapter finds it by measuring the caller rather than the callee. That relocation matters practically, because it changes which functions are worth annotating.
Advanced20 min readC++Part IFundamentalsError Handling
Part I · Module 22 · Chapter 22.6 · noexcept and Where an Exception Must Not Escape
1. A Promise, and a Question
The keyword does two different jobs depending on where it appears.
void promised(int& x) noexcept; // a PROMISE: this will not throw
bool b = noexcept(plain(x)); // a QUESTION: does this promise?
void g() noexcept(COND); // a CONDITIONAL promisenoexcept(plain(probe)) = 0
noexcept(promised(probe)) = 1noexcept on a function is a promise, enforced at run time by std::terminate — §4.
noexcept(expr) is a compile-time bool asking whether expr is declared not to throw. It does not evaluate expr, exactly as Chapter 21.3 §6 measured for decltype.
noexcept(condition) on a function makes the promise conditional, which is how generic code propagates it:
template <typename T>
void doThing(T t) noexcept(std::is_nothrow_move_constructible_v<T>);The promise is part of the function's type, so it is queryable by traits:
is_nothrow_move_constructible<Movable> = 1
is_nothrow_move_constructible<ThrowingMove> = 0
is_nothrow_move_constructible<std::string> = 1That queryability is the point, and §3 is why.
2. The Codegen, Which Is Not What Folklore Says
The usual claim is that noexcept lets the compiler skip generating exception-handling machinery, producing smaller, faster code. Two identical functions, one marked, compiled separately at -O2:
int mayThrow(int n) { Res a{"x"}; Res b{"y"}; std::vector<int> v(n); return /*…*/; }
int wontThrow(int n) noexcept { Res a{"x"}; Res b{"y"}; std::vector<int> v(n); return /*…*/; }FUNCTION __text __gcc_except_tab __compact_unwind TOTAL
may 272 28 128 307
wont 264 32 160 303The noexcept version is not meaningfully smaller. Its __text is 8 bytes less; its exception table is 4 bytes larger and its unwind data 32 bytes larger. The totals are 307 and 303 — indistinguishable.
And the undefined symbols each translation unit needs make the mechanism clear:
may: _Unwind_Resume __cxa_allocate_exception __cxa_free_exception
__cxa_throw __gxx_personality_v0
wont: _Unwind_Resume __cxa_allocate_exception __cxa_free_exception
__cxa_throw __gxx_personality_v0
std::terminate __clang_call_terminate __cxa_begin_catchThe noexcept version needs strictly more. It still references the personality routine and the unwinder, and it additionally needs std::terminate, __clang_call_terminate and __cxa_begin_catch.
That is the whole explanation. noexcept does not remove the unwind path — it adds a handler. The compiler must generate something that catches anything trying to escape and calls terminate, because that is what the promise means. The path does not disappear; its destination changes from "the caller" to "abort the process".
So if you were annotating functions expecting them to get smaller, the measurement says they will not. §3 is where the benefit actually is.
3. The Benefit Is in the Caller
Same experiment, moved one frame out. A function holding one RAII object and calling a callee declared two ways:
int callee(int n); // may throw
int callee(int n) noexcept; // promises not to
int caller(int n) { Res a{"x"}; return callee(n) + (int)a.s.size(); }CALLER OF... __text __gcc_except_tab __compact_unwind TOTAL
may callee 88 12 32 100
wont callee 68 0 32 68may needs: _Unwind_Resume __gxx_personality_v0
wont needs: (nothing)The caller's exception table went from 12 bytes to zero, and its code from 88 bytes to 68 — about 23% smaller — and it no longer references the personality routine or the unwinder at all.
The assembly shows exactly why:
may: wont:
bl callee bl callee
bl Res::~Res bl Res::~Res
bl Res::~Res <--
bl _Unwind_Resume <--The may-throw caller contains two calls to Res's destructor: one on the normal path and one in a landing pad — the cleanup code the unwinder jumps to if callee throws — followed by _Unwind_Resume to continue unwinding. The noexcept caller has one, because there is no path on which callee throws, so there is nothing to clean up on and nothing to resume.
That is the real benefit, and it has a shape worth stating: noexcept does not optimise the function it is on; it optimises every function that calls it. Which means the annotation pays in proportion to how often the function is called, and it pays most on a small function called from many places — exactly the operations §5 lists.
4. Breaking the Promise
static void promised() noexcept { throw std::runtime_error("escaped"); }
int main() {
try { promised(); }
catch (const std::exception& e) { std::printf("CAUGHT: %s\n", e.what()); }
std::printf("returned normally\n");
}The compiler warns, at -Wall:
warning: 'promised' has a non-throwing exception specification but can still throwAnd at run time:
about to call a noexcept function that throws
libc++abi: terminating due to uncaught exception of type std::runtime_error: escaped
run exit=134 (134 = SIGABRT)Neither the catch nor the line after it printed. There was a perfectly good handler, matching the exact type thrown, one frame away — and it was bypassed. std::terminate was called, the process aborted with SIGABRT.
Two things follow.
noexcept is not a hint. It is an assertion whose violation ends the process. Chapter 22.1 §8 listed this among the constrained situations for exactly this reason: marking something noexcept is a decision about failure strategy, not an annotation.
So do not mark a function noexcept unless it truly cannot throw. An "optimistic" noexcept converts a catchable failure into an abort. That is strictly worse than no annotation.
The compiler's warning only fires when it can see a throw statement in the body. A function that calls something which might throw gets no warning, which is the common case and the real hazard.
5. Where It Is Required Rather Than Advisable
Four places, and the first is the frozen purpose's third question.
Destructors. §6. They are implicitly noexcept and a throw from one during unwinding terminates.
Move constructors and move assignment. Chapter 19.3 §3 measured std::vector choosing to move or copy on reallocation based on is_nothrow_move_constructible, and Chapter 22.5 §6 explained why: with a throwing move it cannot offer the strong guarantee, so it copies instead. Your noexcept is what makes the container both fast and safe.
swap. Chapter 22.5 §5 — the strong guarantee is built on a no-throw commit, and swap is the commit.
Deallocation functions, and anything a destructor calls. A close() reached from a destructor must not throw out of it.
The common thread: these are operations that other guarantees are built on. A throwing move does not merely make your type slow; it removes the strong guarantee from every container operation that touches it.
6. The Destructor Case
A destructor is implicitly noexcept even if its body throws. Measured:
struct Throws { ~Throws() { throw std::runtime_error("x"); } };is_nothrow_destructible<Plain> = 1
is_nothrow_destructible<Throws> = 1 <- STILL 1: implicitly noexceptwarning: '~Throws' has a non-throwing exception specification but can still throw
calling it...
libc++abi: terminating due to uncaught exception of type std::runtime_error: x
run exit=134The trait reports 1 for a destructor that unconditionally throws, because the declaration is implicitly noexcept — the trait describes the promise, not the body. The compiler warns, and the throw terminates.
It is possible to opt out with noexcept(false), and the result is instructive:
struct Bad { ~Bad() noexcept(false) { throw std::runtime_error("from a destructor"); } }; A. a destructor that throws, with NO other exception in flight
caught: from a destructor (this CAN work)
B. the same destructor while another exception is UNWINDING
libc++abi: terminating due to uncaught exception of type std::runtime_error
run exit=134With no exception in flight, the throw was caught normally. During unwinding, the same destructor terminated the process.
The reason is that there would be two exceptions propagating at once and the language has no way to represent that — no "both" to hand a handler, and no basis for choosing. So std::terminate.
Which gives the rule in its correct form: a destructor may throw when nothing else is propagating, and must not when something is — and since a destructor cannot tell which situation it is in, it must never throw. Chapter 22.4 §7's explicit close() is the design that follows.
7. What to Mark, and What Not To
The annotation is not free in maintenance terms — it is part of your interface, and removing it later breaks callers who relied on it — so the list is deliberately short.
Mark it:
- destructors (implicit, but writing it documents intent);
- move constructor and move assignment, whenever they genuinely cannot throw;
swap;- simple observers:
size(),empty(),get(), aconstexpraccessor; - anything a destructor calls.
Do not mark it:
- anything that allocates — which includes most copy constructors and anything touching a
std::stringor container; - anything calling a function you do not control;
- anything that might grow a throwing path later, unless you are prepared to keep the promise;
- a function whose "cannot throw" you have not actually checked.
And use the conditional form in generic code, so the promise is derived rather than asserted:
void swap(Widget& a, Widget& b) noexcept(std::is_nothrow_swappable_v<Impl>);The honest default for ordinary application functions is no annotation. §3's measurement says the payoff is in callers, which means the value concentrates in small, frequently called operations — and those are exactly the list above.
8. noexcept and the Rest of the Module
This chapter is the keystone, so it is worth collecting the dependencies.
Chapter 22.3's unwinding destroys objects by calling destructors. If one throws during that, there is no defined behaviour to have — §6.
Chapter 22.4's guards release resources in destructors, so the same applies: a guard whose release can fail needs close().
Chapter 22.5's strong guarantee needs a commit that cannot fail. swap is it.
Chapter 19.3 §3's measured move-or-copy decision is is_nothrow_move_constructible being queried.
Chapter 22.1 §8 listed noexcept among the four constrained situations, because it converts propagation into termination.
The pattern across all five: noexcept is where the type system records something about throwing. It is the only place — Chapter 22.2 §8 noted that a signature says nothing about which exceptions a function throws; C++17 removed dynamic exception specifications entirely. So noexcept is a one-bit channel, and it carries the one bit that other guarantees need.
9. The Table
| a may-throw function | a noexcept function | |
|---|---|---|
__text | 272 | 264 (measured) |
__gcc_except_tab | 28 | 32 — larger (measured) |
__compact_unwind | 128 | 160 — larger (measured) |
| total object size | 307 | 303 — indistinguishable |
| needs the personality routine | yes | yes, still (measured) |
| additionally needs | — | std::terminate, __clang_call_terminate, __cxa_begin_catch (measured) |
| caller of a may-throw callee | caller of a noexcept callee | |
|---|---|---|
__text | 88 | 68 — 23% smaller (measured) |
__gcc_except_tab | 12 | 0 (measured) |
| needs personality / unwinder | yes | no (measured) |
| RAII destructor calls emitted | 2 (normal + landing pad) | 1 (measured) |
| breaking the promise | result |
|---|---|
a throw the compiler can see | warns at -Wall (measured) |
| a throw via a call it cannot see | no warning |
| at run time | std::terminate, SIGABRT, exit 134 — a matching handler one frame away was bypassed (measured) |
a destructor throwing, nothing in flight, noexcept(false) | can be caught (measured) |
| a destructor throwing during unwinding | terminate (measured) |
is_nothrow_destructible of a throwing destructor | 1 — the trait describes the declaration (measured) |
10. What to Actually Do
- Mark destructors, moves,
swapand simple observersnoexcept. That is where the caller-side benefit concentrates. - Do not mark anything that allocates, or that calls code you do not control.
- Never mark optimistically. Measured: a throw bypasses a matching handler and aborts.
- Use the conditional form in generic code so the promise is derived from a trait.
- Give a destructor's failing cleanup an explicit
close()— Chapter 22.4 §7. - Never throw from a destructor, even with
noexcept(false), because it cannot know whether unwinding is in progress. - Make your move
noexceptso containers can be both fast and strong — Chapter 19.3 §3 and Chapter 22.5 §6. - Do not expect the marked function to shrink. Measured: it grows slightly.
- Treat the annotation as part of the interface. Removing it later breaks callers who relied on it.
- Do not rely on the compiler's warning. It fires only for a visible
throw.
11. Mistakes Worth Making Once
An optimistic noexcept. Measured: terminate, SIGABRT, and a perfectly good handler never ran.
noexcept on something that allocates. std::bad_alloc becomes an abort.
Throwing from a destructor. Measured: catchable with nothing in flight, terminate during unwinding — and a destructor cannot tell which.
Trusting is_nothrow_destructible. Measured 1 for a destructor that throws unconditionally; it reports the declaration.
Expecting a smaller function. Measured: slightly bigger, with three extra runtime dependencies.
Forgetting noexcept on a move and then wondering why a vector of your type copies on growth — Chapter 19.3 §3.
Removing a noexcept from a published interface. Callers' strong guarantees were built on it.
Relying on the warning to catch an over-promise. It only sees a literal throw.
12. Interview Relevance
The frozen curriculum marks this chapter Critical, and the codegen question is unusually good because the honest answer contradicts the common one.
"What does noexcept do?" It promises the function will not throw, enforced by std::terminate. The strong answer continues: it does not remove the function's unwind path — measured, it adds a terminating handler and the function gets slightly bigger — and the benefit is in the caller, whose exception table went from 12 bytes to zero and which stopped needing the personality routine.
"What happens if a noexcept function throws?" std::terminate. Measured: a matching handler one frame away was bypassed, SIGABRT, exit 134. Not catchable.
"Why must a destructor not throw?" Because it runs during unwinding, and two simultaneous exceptions have no representation. Measured: catchable with nothing in flight, terminate during unwinding — and a destructor cannot distinguish the two.
"Is a destructor noexcept by default?" Yes, implicitly — and measured, is_nothrow_destructible reports 1 even for one that throws unconditionally.
"Where should you use it?" Destructors, moves, swap, simple observers — the operations other guarantees are built on. Not on anything that allocates.
"Why does a noexcept move matter?" Because vector reallocation queries it and copies instead when it is absent, losing both speed and the strong guarantee.
13. What You Should Be Able to Say Now
- That
noexcepton a function is a promise andnoexcept(expr)is a non-evaluating question, and that the promise is part of the type. - That the marked function is not smaller — measured 264 vs 272
__textbut a larger exception table and unwind data, totals indistinguishable. - That it adds
std::terminate,__clang_call_terminateand__cxa_begin_catchas dependencies rather than removing any. - That the benefit is in the caller:
__text88 → 68, exception table 12 → 0, personality routine no longer needed, and two RAII destructor calls reduced to one. - That the payoff therefore scales with call count, concentrating on small, hot operations.
- That breaking the promise bypasses a matching handler and aborts — measured exit 134.
- That the compiler warns only for a visible
throw. - That a destructor is implicitly
noexcept, thatis_nothrow_destructiblereports the declaration rather than the body, and thatnoexcept(false)makes a throw catchable only when nothing else is propagating. - The four places it is required, and why they are the ones other guarantees rest on.
14. Exercises
Exercise 1 — the codegen. Reproduce §2 on your platform. Report the section sizes and the undefined-symbol lists, and say whether your compiler agrees that the marked version is larger.
Exercise 2 — the caller. Reproduce §3. Then add a second RAII object to the caller and re-measure, to see whether the saving scales with the number of objects.
Exercise 3 — the abort. Build §4 and confirm the handler is bypassed. Then make the throw come from a function the compiler cannot see and verify that the warning disappears.
Exercise 4 — the destructor. Reproduce §6's two cases. Then write a destructor that calls something which may throw, and show that the compiler does not warn.
Exercise 5 — the trait. Check is_nothrow_destructible, is_nothrow_move_constructible and is_nothrow_swappable for five of your own types. Fix any whose move is not noexcept and could be.
Exercise 6 — move or copy. Build a type with and without a noexcept move, grow a vector of each past capacity, and count moves against copies.
Exercise 7 — conditional. Write a generic wrapper whose swap is noexcept exactly when the wrapped type's is, and verify both cases with static_assert.
Exercise 8 — the interface cost. Publish a noexcept function, write a caller that depends on it for a strong guarantee, then remove the noexcept and determine what breaks and whether anything warns.
15. Summary
noexcepton a function is a promise enforced bystd::terminate;noexcept(expr)is a non-evaluating compile-time question; the promise is part of the type and queryable by traits.- Measured, against the folklore: the marked function is NOT smaller.
__text264 vs 272, but__gcc_except_tab32 vs 28 and__compact_unwind160 vs 128 — totals 303 and 307. - Measured: it needs MORE runtime support — everything the throwing version needs plus
std::terminate,__clang_call_terminateand__cxa_begin_catch. It adds a terminating handler rather than removing the unwind path. - Measured: the benefit is in the CALLER —
__text88 → 68, exception table 12 → 0, no personality routine or unwinder referenced, and two RAII destructor calls (normal path plus landing pad) reduced to one. - So the payoff scales with call count, concentrating on small, frequently called operations — which is independently the list of operations other guarantees rest on.
- Measured: breaking the promise bypassed a matching handler one frame away and aborted with SIGABRT, exit 134. The compiler warns only for a literal
throwin the body. - Measured: a destructor is implicitly
noexceptandis_nothrow_destructiblereports 1 even for one that throws unconditionally. - Measured: with
noexcept(false), a destructor's throw is catchable when nothing is propagating and terminates during unwinding — and a destructor cannot tell which situation it is in, so it must never throw. - Mark destructors, moves,
swapand simple observers. Never mark anything that allocates, and never optimistically. - The annotation is part of the interface; removing it breaks callers whose guarantees were built on it.
Six chapters have been about the exceptional half of Chapter 22.1's taxonomy. The expected half has been deferred since then, and Chapter 22.7 takes it up: reporting a failure that is a normal outcome, through the return value. It covers std::optional where absence needs no explanation, a status enum where it does, and the result types that carry a value or a reason — and it returns to Chapter 22.1's measurement to make the case concrete, because an expected failure routed through a throw costs roughly eleven thousand times what returning it costs.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
