Skip to content

80 Advanced The Hundred

The Destructor That Ends It All

an exception with nowhere to go

struct Connection {
    ~Connection() { throw std::runtime_error("flush failed"); }
};

int main() {
    try {
        Connection c;
        std::cout << "sending data" << std::endl;
    } catch (const std::exception& e) {
        std::cout << "caught: " << e.what() << "\n";
    }
}

Run it. What does it print?

Answer

sending data — then the catch block is skipped and the whole program aborts (GCC):

terminate called after throwing an instance of 'std::runtime_error'
  what():  flush failed
Aborted (core dumped)

Why

Since C++11, destructors are implicitly noexcept — and an exception escaping a noexcept function never reaches the enclosing try; it calls std::terminate on the spot. The rule exists because destructors run during stack unwinding: if one throws while another exception is already in flight, there are two live exceptions and nowhere for either to go. Pre-C++11 that collision already meant terminate; C++11 just baked the consequence into the destructor's signature. ~Connection() noexcept(false) opts back out — the catch above would then work — but any throw during unwinding still terminates, so it merely relocates the landmine.

GCC flags this even without -Wall: -Wterminate warns "'throw' will always call 'terminate'" and notes "in C++11 destructors default to 'noexcept'".

The fix

Give cleanup that can fail a separate, throwing close(); the destructor is only a fallback:

void close() {                  // callers who care about failure call this
    closed = true;
    if (!flush()) throw std::runtime_error("flush failed");
}
~Connection() {
    if (!closed) try { close(); } catch (...) {}  // last resort: swallow (or log)
}

Takeaway: destructors are noexcept: report failure before the object dies — during destruction, nobody can hear you throw.

Try it: g++ -std=c++17 main.cpp -o demo && ./demo

Open in Compiler Explorer ↗ Quiz this entry