Error Handling
2 min read
Error Handling
C++ provides exceptions for signaling and handling errors. Exceptions decouple error detection from error handling and unwind the stack automatically, triggering destructors (and thus RAII cleanup) along the way.
Throwing and catching
#include <iostream>
#include <stdexcept>
double divide(double a, double b) {
if (b == 0.0) {
throw std::invalid_argument("division by zero");
}
return a / b;
}
int main() {
try {
std::cout << divide(10.0, 2.0) << "\n"; // 5
std::cout << divide(10.0, 0.0) << "\n"; // throws
}
catch (const std::invalid_argument& e) {
std::cout << "Error: " << e.what() << "\n";
}
}
The standard exception hierarchy
std::exception
├── std::logic_error — precondition violations, programming errors
│ ├── std::invalid_argument
│ ├── std::out_of_range
│ └── std::domain_error
└── std::runtime_error — errors detectable only at runtime
├── std::range_error
├── std::overflow_error
└── std::underflow_error
Catch the most specific type you can handle. A catch for std::exception handles everything; a catch for std::out_of_range handles only that type:
try { /* ... */ }
catch (const std::out_of_range& e) { /* specific */ }
catch (const std::runtime_error& e) { /* broader */ }
catch (const std::exception& e) { /* everything */ }
catch (...) { /* truly anything, including non-std */ }
Custom exception types
Derive from std::exception or one of its subclasses:
#include <stdexcept>
#include <string>
class DatabaseError : public std::runtime_error {
int code_;
public:
DatabaseError(int code, const std::string& msg)
: std::runtime_error(msg), code_(code) {}
int code() const { return code_; }
};
void query(const std::string& sql) {
if (sql.empty()) throw DatabaseError(400, "empty query");
}
int main() {
try {
query("");
}
catch (const DatabaseError& e) {
std::cerr << "[" << e.code() << "] " << e.what() << "\n";
}
}
noexcept
Mark functions that never throw noexcept. The compiler can optimize them better, and the standard library relies on this for move operations:
int safe_add(int a, int b) noexcept { return a + b; }
If a noexcept function does throw, std::terminate is called immediately. Destructors are implicitly noexcept — never throw from one.
Re-throwing
try {
risky_operation();
}
catch (const std::exception& e) {
log_error(e.what());
throw; // re-throw the same exception — preserves the original type
}
Bare throw; re-throws the current exception. throw e; throws a copy (and may slice if e is a base class reference).
Exception safety levels
Functions should aim for one of these guarantees:
| Level | Guarantee |
|---|---|
| No-throw | Never throws; noexcept |
| Strong | If it throws, the program state is unchanged (commit-or-rollback) |
| Basic | If it throws, the program is in a valid (though possibly changed) state; no leaks |
| None | No guarantee; avoid |
RAII makes the basic guarantee easy to achieve: destructors clean up even when exceptions unwind the stack.
Learn modern C++ through working code. Each chapter introduces one concept — variables, functions, classes, templates, smart pointers, concurrency — with a clear example, a line-by-line explanation, and notes on how it applies in real programs.
View book →