C++ exceptions separate failure reporting from the normal return path, while RAII ensures owned resources are released during stack unwinding. Throw exceptions for failures a caller can handle at a suitable boundary, not for expected loop conditions or ordinary optional results.
A boundary can add domain context with std::throw_with_nested or a new exception while preserving the original cause. This small example validates before division.
#include <iostream>
#include <stdexcept>
double ratio(double total, double count) {
if (count == 0) throw std::invalid_argument{"count must not be zero"};
return total / count;
}
int main() {
try {
std::cout << ratio(10, 0) << '\n';
} catch (const std::invalid_argument& error) {
std::cerr << "input error: " << error.what() << '\n';
}
}
input error: count must not be zero
If an exception escapes a noexcept function, std::terminate is called. Mark functions noexcept only when the implementation and called operations support that guarantee; destructors should not allow exceptions to escape.
The basic guarantee preserves invariants and prevents leaks after failure. The strong guarantee leaves the observable operation unchanged, often by building a temporary result and committing with a non-throwing swap. The no-throw guarantee is required for destructors and important cleanup paths.
Catch by const reference to avoid slicing, order handlers from specific to general, and add context without discarding the original cause. Never let a destructor throw during unwinding. Define one top-level boundary that turns uncaught failures into a controlled exit or service response.
An exception transfers control, but destructors for fully constructed automatic objects still run as the stack unwinds. Put resource release in RAII owners rather than duplicating cleanup in every catch block. Catch only where code can recover, translate the failure, or add useful context.
Throw values by value and catch polymorphic standard exceptions by const reference. A catch-all at the application boundary may log and terminate an operation, but library code should not erase the original failure merely to return a generic value.
#include <iostream>
#include <stdexcept>
struct Guard {
~Guard() { std::cout << "cleanup\n"; }
};
void work() {
Guard guard;
throw std::runtime_error("failed");
}
int main() {
try { work(); }
catch (const std::exception& error) { std::cout << error.what() << '\n'; }
}
cleanup
failed
The guard destructor runs before control reaches the handler, proving that cleanup follows object lifetime rather than duplicated error paths.
Try this next
0 of 2 completed
It avoids copying and preserves the dynamic exception type without allowing the handler to modify it.
Stack objects are destroyed during unwinding, so their destructors can release files, locks, and memory.
The program calls std::terminate instead of passing the exception to an outer handler.
Explore 500+ free tutorials across 20+ languages and frameworks.