Skip to content
VLSI Mentor

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 promise
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
noexcept(plain(probe))     = 0
noexcept(promised(probe))  = 1

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
is_nothrow_move_constructible<Movable>      = 1
is_nothrow_move_constructible<ThrowingMove> = 0
is_nothrow_move_constructible<std::string>  = 1

That 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 /*…*/; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
FUNCTION     __text    __gcc_except_tab   __compact_unwind   TOTAL
may          272       28                 128                307
wont         264       32                 160                303

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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_catch

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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(); }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
CALLER OF...           __text    __gcc_except_tab   __compact_unwind   TOTAL
may callee             88        12                 32                 100
wont callee            68        0                  32                 68
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
may    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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: 'promised' has a non-throwing exception specification but can still throw

And at run time:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
struct Throws { ~Throws() { throw std::runtime_error("x"); } };
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
is_nothrow_destructible<Plain>  = 1
is_nothrow_destructible<Throws> = 1  <- STILL 1: implicitly noexcept
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: '~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=134

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
struct Bad { ~Bad() noexcept(false) { throw std::runtime_error("from a destructor"); } };
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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=134

With 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(), a constexpr accessor;
  • anything a destructor calls.

Do not mark it:

  • anything that allocates — which includes most copy constructors and anything touching a std::string or 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 functiona noexcept function
__text272264 (measured)
__gcc_except_tab2832 — larger (measured)
__compact_unwind128160 — larger (measured)
total object size307303 — indistinguishable
needs the personality routineyesyes, still (measured)
additionally needs—std::terminate, __clang_call_terminate, __cxa_begin_catch (measured)
caller of a may-throw calleecaller of a noexcept callee
__text8868 — 23% smaller (measured)
__gcc_except_tab120 (measured)
needs personality / unwinderyesno (measured)
RAII destructor calls emitted2 (normal + landing pad)1 (measured)
breaking the promiseresult
a throw the compiler can seewarns at -Wall (measured)
a throw via a call it cannot seeno warning
at run timestd::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 unwindingterminate (measured)
is_nothrow_destructible of a throwing destructor1 — the trait describes the declaration (measured)

10. What to Actually Do

  • Mark destructors, moves, swap and simple observers noexcept. 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 noexcept so 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 noexcept on a function is a promise and noexcept(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 __text but a larger exception table and unwind data, totals indistinguishable.
  • That it adds std::terminate, __clang_call_terminate and __cxa_begin_catch as dependencies rather than removing any.
  • That the benefit is in the caller: __text 88 → 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, that is_nothrow_destructible reports the declaration rather than the body, and that noexcept(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

  • noexcept on a function is a promise enforced by std::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. __text 264 vs 272, but __gcc_except_tab 32 vs 28 and __compact_unwind 160 vs 128 — totals 303 and 307.
  • Measured: it needs MORE runtime support — everything the throwing version needs plus std::terminate, __clang_call_terminate and __cxa_begin_catch. It adds a terminating handler rather than removing the unwind path.
  • Measured: the benefit is in the CALLER — __text 88 → 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 throw in the body.
  • Measured: a destructor is implicitly noexcept and is_nothrow_destructible reports 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, swap and 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.