C++
Exception Handling
C has one universal error-handling convention: return a special value (or an error code) and hope every
caller actually checks it. Exceptions are C++'s structured alternative — a function that hits a problem
can throw, immediately unwinding up the call stack until something explicitly agrees to
catch it, with every destructor along the way still running automatically.
- What it is
- A mechanism for signaling and handling errors by unwinding the call stack, rather than checking a return value at every call site.
- Mental model
- A fire alarm, not a note left on a desk —
throwstops everything immediately and propagates upward until acatchblock agrees to handle it, instead of relying on every single caller to remember to check. - Key numbers
- An uncaught exception terminates the program immediately, via
std::terminate— there is no silent fallthrough the way an unchecked C error code has. - Reach for it when
- An error is genuinely exceptional (a file that should exist doesn't, a network call fails) and the calling code several layers up is what actually knows how to recover — not for routine, expected control flow.
try, catch, throw
#include <stdexcept>
float divide(float a, float b) {
if (b == 0) {
throw std::runtime_error("division by zero"); // throw — stop, unwind, look for a catch
}
return a / b;
}
int main() {
try {
float result = divide(10, 0);
std::cout << result << std::endl; // never reached
} catch (const std::runtime_error &e) {
std::cout << "Error: " << e.what() << std::endl; // Error: division by zero
}
return 0;
}
divide never has to be called with a return-value check at every call site — the caller
(here, several lines away in main) is where the actual recovery decision gets made, and
every function in between needs no error-handling code of its own at all.
Stack unwinding and RAII
When an exception is thrown, C++ walks back up the call stack looking for a matching catch,
running the destructor of every local object along the way — this is exactly why
RAII matters so much in C++ specifically: a
resource wrapped in an RAII object is released correctly even when an exception skips the rest of the
function entirely.
void process() {
FileHandle f("data.txt"); // RAII-managed — see Constructors, Destructors & RAII
riskyOperation(); // throws
// nothing after this line runs
} // ~FileHandle() STILL runs during unwinding — the file is still closed correctly
What to throw
<stdexcept> provides a small hierarchy of standard exception types — throwing one of
these (or your own class derived from std::exception) is standard practice over throwing a
plain number or string:
std::runtime_error- A general error only detectable at runtime — the most commonly thrown type.
std::invalid_argument- A function argument that doesn't make sense (e.g. a negative size).
std::out_of_range- An index or key outside a valid range — thrown by
std::vector::at(), for example. std::logic_error- A violated precondition that, in principle, could have been checked before calling.
Exceptions vs. C-style error codes
| C-style error codes | Exceptions | |
|---|---|---|
| Easy to ignore | Yes — an unchecked return value fails silently | No — an uncaught exception terminates the program loudly |
| Propagates automatically | No — every intermediate function must check and re-return it | Yes — unwinds automatically until caught |
| Runtime cost when not thrown | None | Effectively none on modern compilers |
| Runtime cost when thrown | N/A | Real, non-trivial — not meant for routine control flow |
Real-world applications
- File and network I/O — a failed file open or a dropped connection is a natural fit for an exception, since the code several layers up the call stack is usually what actually decides how to recover.
- Library input validation — throwing
std::invalid_argumentfrom a constructor is the standard way to reject bad input in library code, since a constructor has no return value to signal failure with otherwise. - Desktop and server application error handling — most non-embedded C++ codebases (browsers, database engines, desktop apps) rely on exceptions as their primary error-handling mechanism.
Gotchas
- Exceptions are genuinely unpopular in embedded and real-time C++ for concrete reasons: unpredictable worst-case timing during unwinding, and a real code-size cost from the exception-handling machinery the compiler generates — the subject of the next, final chapter.
- A destructor that throws during stack unwinding caused by another exception calls
std::terminateimmediately — exactly the same rule mentioned in Constructors, Destructors & RAII's gotchas: destructors should never throw. - Catching
...(literally any exception, of any type) without at least logging what was caught hides real bugs — prefer catching specific, expected exception types. - Throwing an exception for routine, expected outcomes (like "value not found" in a lookup that's often expected to miss) is a common misuse — reserve exceptions for genuinely exceptional, unexpected failures, and use an ordinary return value for expected outcomes.