Module 13 — Misconceptions and Mistakes¶
Two different problems, fixed two different ways.
A misconception is a wrong mental model, and only evidence removes it — so each one below is paired with a program that contradicts it. A mistake is a concrete error, and information fixes it — so each one below comes with the message this toolchain actually printed and the corrected code.
Every diagnostic quoted here was produced by compiling the snippets shown, with the course's flags.
Part 1 — Misconceptions¶
1. "Exceptions are for normal control flow."¶
They are convenient: a find() that throws is shorter to write than one that returns
std::optional. That is exactly the trap.
An expected outcome — a search that finds nothing, a cache miss, a lock that is busy — is a normal result that the caller will handle immediately. A failure is a request the operation cannot satisfy. The library system contains both, and they get different mechanisms:
std::optional<Item> find(int id) const; // expected: `if (!item)` at the call site
void require(int id) const; // failure: throws ItemRequired but missing
The test: if throwing became free, would you still want the caller to handle this in the same breath as the call? If yes, it is an answer, not an exception.
2. "catch (...) makes my program safe."¶
It makes the program continue, which is not the same thing.
Everything that made the failure diagnosable has been discarded: no type, no message, no clue. If
processLoan half-updated the ledger before failing, the program now continues with a broken
invariant and no record of it. Catching everything and continuing is worse than crashing, because
crashing at least tells you.
Legitimate uses are narrow: a boundary that must release something by hand (which Module 11 removed
the need for), and a handler that logs and immediately rethrows with throw;.
3. "I do not need RAII if I catch everything."¶
The catch block can release the resource — for the failures it sees. The three it misses:
void reload(const std::string& path) {
HANDLE file = openCatalogue(path);
try {
load(file); // 1. a throw in here is caught, and released
if (bad(file)) { close(file); return; } // 2. an early return must remember too
close(file);
} catch (...) {
close(file); // 3. and this releases TWICE if close() is not careful
throw;
}
}
And a resource acquired before the try — in a constructor body — is released by none of them.
There is exactly one cleanup mechanism that cannot be skipped, and it is a destructor. catch is for
deciding, not for releasing.
4. "Exceptions are slow, so I should avoid them."¶
The claim is usually stated the wrong way round. What is expensive is throwing; the cost of a call that could throw but does not is negligible — it is roughly the same as a branch. Which means:
- throwing in a loop that runs a million times is expensive, and it is also control flow, so it is wrong for the second reason;
- throwing on a failure that happens once, three frames below the caller who can handle it, costs nothing that matters and removes error-handling code from every frame in between.
The example program 01-error-codes-versus-exceptions puts it plainly: a call that succeeds is not
slower for being able to throw.
5. "A destructor can throw like any other function."¶
Since C++11 a destructor is noexcept by default, and a throw inside one is not a control-flow
decision — it is std::terminate:
struct Loader {
~Loader() noexcept(false) { // taking the promise back, explicitly
throw std::runtime_error("cannot close the catalogue file");
}
};
terminate called after throwing an instance of 'std::runtime_error'
what(): cannot close the catalogue file
And when this happens during unwinding, the original failure is lost — the message names the destructor's exception, not the one that started the unwinding. Two failures cannot both be reported, so the standard ends the program instead of continuing with a state neither of them describes.
A destructor may log, swallow, or offer an explicit close() that callers invoke when they want
to see the error. It may not throw.
6. "noexcept is a comment for readers."¶
It is checked, it is part of the interface, and it is read by the standard library:
void save() noexcept; // promise
void save() noexcept(false); // explicitly withdrawn (which is the default, stated out loud)
Break it and the program terminates without unwinding, because the caller has already been told the call cannot fail and may have arranged things on that basis. Inherit it wrongly and the compiler stops you:
error: looser exception specification on overriding virtual function
'virtual void MemoryRepository::save() noexcept (false)'
note: overridden function is 'virtual void Repository::save() noexcept'
And the library uses the answer: std::vector moves elements during growth only if their move is
noexcept (Module 10). A noexcept you cannot honour is worse than no noexcept at all.
7. "If it compiles, the failure path is handled."¶
Compilation proves nothing about a path that has never executed. A program can work on every good input and corrupt its state on one bad one, and no compiler will mention it.
The only evidence is a run: a deliberate throw, counters that prove acquisition and release balance, and an assertion that the invariants still hold afterwards. Lab 13 requires exactly that, as two runs of the same sequence.
8. "Exceptions are an alternative to validation."¶
Throwing on bad input feels like handling it. It postpones it.
Validation belongs at the boundary — the one place that receives untrusted data — and after it, everything downstream may assume the data is good. Throw, by all means, from the validator; but the work of validation is the check, not the throw. Module 4's rule (enforce the invariant where it can be enforced) applies unchanged to failure.
Part 2 — Common programming mistakes¶
Symptoms, and what actually causes them¶
| Symptom | Cause |
|---|---|
The message printed by a catch has lost its detail, and the handler cannot tell two failures apart |
Caught by value: the exception was sliced. Catch by const& |
| A specific handler never runs | The base-class handler is written first; the first matching handler wins |
| A resource leaks only when an exception occurs | The resource was not owned by an object, so unwinding could not release it |
| A resource is released twice | A catch block released something RAII already released |
The program ends with terminate called after throwing ... |
An exception escaped main, or a destructor threw while another exception was in flight |
The terminate message names the wrong failure |
A destructor threw during unwinding: the original exception is gone by definition |
| Nothing at all was printed before the program ended | std::cout was still buffered when std::terminate aborted the process. Use std::cerr, or flush |
error: looser exception specification on overriding virtual function |
An override throws where the base declared noexcept. A virtual function's exception specification is part of its contract |
warning: 'throw' will always call 'terminate' [-Wterminate] |
A throw inside a function (or destructor) that promised not to throw. The compiler can see the termination in advance |
warning: catching polymorphic type ... by value [-Wcatch-value=] |
catch (LibraryError error) — a copy, and the derived part is lost |
warning: exception of type 'ItemUnavailable' will be caught by earlier handler [-Wexceptions] |
Handler order: most-derived must come first |
After a catch, the program behaves oddly |
The operation provided only the basic guarantee: nothing leaked and the object is valid, but its contents changed |
| State is corrupted after a failed operation | Same cause; the fix is to validate first, or to work on a copy and swap |
| An exception thrown in a constructor leaves no object, but something leaked | The resource was acquired in the constructor body rather than owned by a member |
error: loop variable ... binds to a temporary constructed from type 'const char* const' [-Werror=range-loop-construct] |
A const std::string& loop variable over a braced list of string literals. Take the element by value, or hold real std::strings |
Worked mistakes¶
Each of the following was compiled on this toolchain; the messages are quoted as they appeared, and the line numbers belong to the small file each extract came from.
M1. Catching by value¶
struct LibraryError : std::runtime_error { /* ... */ };
try {
throw LibraryError("item already on loan");
} catch (LibraryError error) { // by value
std::cout << error.what() << "\n";
}
warning: catching polymorphic type 'struct LibraryError' by value [-Wcatch-value=]
12 | } catch (LibraryError error) {
| ^~~~~
It still compiles, and at this level of the hierarchy it still prints the right message — which is
what makes the mistake survive. What is lost is the derived type: catch a CorruptCatalogue this way
and the handler holds a LibraryError, with no file() and no line(). Fix: catch (const
LibraryError& error).
M2. Handlers in the wrong order¶
} catch (const std::runtime_error& error) { // base: written first
std::cout << "base handler: " << error.what() << "\n";
} catch (const ItemUnavailable& error) { // dead code
std::cout << "derived handler: " << error.what() << "\n";
}
warning: exception of type 'ItemUnavailable' will be caught by earlier handler [-Wexceptions]
14 | } catch (const ItemUnavailable& error) {
| ^~~~~
note: for type 'std::runtime_error'
12 | } catch (const std::runtime_error& error) {
| ^~~~~
The note names the culprit, which is the part worth reading: the compiler is telling you which
handler shadows which. Order handlers most-derived first, catch (...) last.
M3. An override that throws where the base said noexcept¶
class Repository {
public:
virtual void save() noexcept = 0;
};
class MemoryRepository : public Repository {
public:
void save() noexcept(false) override { /* ... */ } // looser
};
error: looser exception specification on overriding virtual function
'virtual void MemoryRepository::save() noexcept (false)'
12 | void save() noexcept(false) override {
| ^~~~
note: overridden function is 'virtual void Repository::save() noexcept'
6 | virtual void save() noexcept = 0;
| ^~~~
This one is a compile error, and it should be: a caller holding a Repository& was promised that
save() cannot fail, and the derived class would break that promise on its behalf. The fix is to
change the base interface deliberately (and then every caller must be revisited) or to make the
override genuinely not throw.
M4. A destructor that throws during unwinding¶
struct Loader {
~Loader() noexcept(false) {
throw std::runtime_error("cannot close the catalogue file");
}
};
Compiles cleanly — noexcept(false) is an explicit statement, so the compiler does not warn — and at
run time:
terminate called after throwing an instance of 'std::runtime_error'
what(): cannot close the catalogue file
The message is the wrong one. The exception that was actually propagating was the original
failure; that is unrecoverable information now, and the catch block that would have logged it never
ran. This is the mistake that makes the standard's severity about throwing destructors sensible.
M5. A throw inside a function that promised not to throw¶
warning: 'throw' will always call 'terminate' [-Wterminate]
5 | throw std::runtime_error("broke the promise");
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Under this course's -Werror build the warning is an error, so the mistake never reaches a run: the
compiler refuses. That is the difference between a promise a reader is asked to trust and a promise
the compiler checks.
M6. A reference element over a list of literals¶
Small, common, and not really about exceptions — it is here because it appears the moment you write a loop over test inputs in a failure-handling exercise:
error: loop variable 'typed' of type 'const std::string&' {aka
'const std::__cxx11::basic_string<char>&'} binds to a temporary constructed from type
'const char* const' [-Werror=range-loop-construct]
125 | for (const std::string& typed : {"3", "", "seven", "-2"}) {
| ^~~~~
note: use non-reference type 'const std::string' to make the copy explicit
The braced list holds const char*; each element needs a std::string constructed from it, so a
reference would bind to a temporary whose lifetime is the loop body. The compiler refuses rather than
quietly allowing it. Take the element by value — the copy is the point.
The three rules worth memorising¶
- Catch by
const&, most-derived handler first, and never swallow silently. - Express ownership in types, not in
catchblocks — unwinding is the only cleanup that cannot be skipped. - Write down each operation's guarantee (nothrow, strong, basic) next to its declaration, and test the failure path, not just the success path.
Continue¶
Previous: Worked examples · Next: Debugging · Module home: Module 13