Exception Handling
When a function fails ten calls deep, exceptions let it say so directly to whichever caller actually knows what to do about it, instead of forcing every function in between to check and re-check a return code it can't handle.
A function that detects a problem — a file that won't open, an index that's out of range, memory that can't be allocated — is rarely the function that knows what to do about it. In C, the usual answer is to return an error code and hope every caller up the chain checks it, propagates it, and eventually someone handles it. Miss one check anywhere in that chain and the error silently vanishes. C++ exceptions solve this by separating detection from handling entirely: code that hits a problem throws, execution unwinds the stack automatically past every intervening function, running destructors as it goes, until it reaches a catch block prepared to deal with that specific kind of failure. This page covers try/throw/catch mechanics, what stack unwinding actually does to your objects, how to catch multiple exception types correctly, building custom exception classes on std::exception, the noexcept specifier, a preview of exception safety guarantees, why RAII is what makes all of this safe rather than catastrophic, and the tradeoffs against plain error codes — including why some embedded and real-time codebases turn exceptions off entirely.
The Problem: Errors Detected Far From Where They Can Be Handled
Consider a chain of calls: main() calls loadConfig(), which calls parseSection(), which calls parseValue(), which discovers the input is malformed. parseValue() itself has no idea what the right response is — maybe main() should print an error and exit, maybe it should fall back to defaults, maybe it should retry with a different file. Only a caller far up the chain knows. With return codes, every function in between — parseSection() and loadConfig() — has to check the return value it got, decide it can't handle it, and manually forward it upward. Forget one check and the failure disappears into a return value nobody looked at.
Nothing forces parseSection() to check parseValue()'s return value. The compiler won't warn (unless you remember [[nodiscard]] on every such function), so this compiles cleanly and quietly uses an uninitialized value when parsing fails.
An exception can't be silently dropped the same way. Throwing forces the program to either handle the failure somewhere on the call stack or terminate loudly — there's no equivalent of quietly ignoring a return value.
bool parseValue(const std::string &text, int &out)
{
if (text.empty())
return false;
out = std::stoi(text);
return true;
}
void parseSection(const std::string &line)
{
int value;
parseValue(line, value); // return value ignored here -- bug!
std::cout << "Parsed: " << value << "\n"; // may print garbage
}try, throw, and catch
A throw expression raises an exception object of any type, though throwing something derived from std::exception is the idiomatic choice. A try block wraps code that might throw, and one or more catch clauses following it handle specific exception types.
divide(10.0, 2.0) succeeds and prints 5.
divide(5.0, 0.0) hits denominator == 0.0 and throws a std::runtime_error.
Execution immediately abandons the rest of the try block -- the third std::cout line never runs.
Control jumps straight to the matching catch (const std::runtime_error &e) clause.
e.what() returns the message string passed to the constructor, so the program prints 'Caught: division by zero'.
As soon as throw executes, normal control flow stops completely -- no more statements in the try block run, no matter how many were left. Control transfers directly to the first catch clause whose type matches (or is a base of) the thrown object's type.
#include <iostream>
#include <stdexcept>
double divide(double numerator, double denominator)
{
if (denominator == 0.0)
throw std::runtime_error("division by zero");
return numerator / denominator;
}
int main()
{
try
{
std::cout << divide(10.0, 2.0) << "\n";
std::cout << divide(5.0, 0.0) << "\n";
std::cout << "This line never runs.\n";
}
catch (const std::runtime_error &e)
{
std::cout << "Caught: " << e.what() << "\n";
}
return 0;
}Stack Unwinding
When a throw happens inside a deeply nested call, C++ doesn't just jump to the catch block and leave everything else as-is. It performs stack unwinding: it walks back up through every function call currently in progress between the throw and the matching catch, and for each one, destroys every local object that was fully constructed in that frame, calling its destructor, before moving up to the next frame.
outer() constructs its Guard, prints 'Constructing outer', then calls middle().
middle() constructs its Guard, prints 'Constructing middle', then calls innermost().
innermost() constructs its Guard, prints 'Constructing innermost', then throws.
Unwinding destroys innermost's Guard first: 'Destroying innermost'.
Then middle's Guard: 'Destroying middle'.
Then outer's Guard: 'Destroying outer'.
Only after all three are destroyed does control reach main()'s catch, which prints 'Caught: failure deep inside'.
Notice the destruction order is the exact reverse of construction order, and it happens for every frame between the throw and the catch -- three whole function calls' worth of local state gets cleaned up automatically, with zero explicit cleanup code written anywhere.
#include <iostream>
#include <stdexcept>
struct Guard
{
std::string label;
explicit Guard(std::string l) : label(std::move(l))
{
std::cout << "Constructing " << label << "\n";
}
~Guard()
{
std::cout << "Destroying " << label << "\n";
}
};
void innermost()
{
Guard g("innermost");
throw std::runtime_error("failure deep inside");
}
void middle()
{
Guard g("middle");
innermost();
}
void outer()
{
Guard g("outer");
middle();
}
int main()
{
try
{
outer();
}
catch (const std::exception &e)
{
std::cout << "Caught: " << e.what() << "\n";
}
return 0;
}Multiple catch Blocks and Catching by Reference
A try block can be followed by several catch clauses, each handling a different exception type. They're tried in the order they're written, top to bottom, and the first one whose type matches (exactly, or as a base class of) the thrown object wins -- so more specific (derived) exception types must be listed before more general (base) ones, or the general one will swallow everything and the specific handler becomes unreachable dead code.
std::out_of_range derives from std::logic_error, which derives from std::exception. Because the std::out_of_range catch clause is listed first, it matches and runs; if the order were reversed, the std::exception clause would catch it first (since std::out_of_range is-a std::exception) and the more specific clause would never fire, and most compilers will warn about that.
Catch clauses should almost always take the exception by const reference (const std::exception &e), not by value. Catching by value copies the exception object and, if the caught type is a base class but the thrown object was actually a derived type, that copy slices off the derived part -- you lose the specific data and even the virtual what() override can behave unexpectedly. Catching by reference binds directly to the original thrown object with no copy and no slicing, and it's exactly as cheap as catching by value would be, so there's no performance argument for value.
#include <iostream>
#include <stdexcept>
#include <vector>
void access(const std::vector<int> &v, std::size_t index)
{
std::cout << v.at(index) << "\n"; // .at() throws std::out_of_range
}
int main()
{
std::vector<int> v{1, 2, 3};
try
{
access(v, 10);
}
catch (const std::out_of_range &e)
{
std::cout << "Out of range: " << e.what() << "\n";
}
catch (const std::exception &e)
{
std::cout << "Some other std::exception: " << e.what() << "\n";
}
return 0;
}std::exception and Custom Exception Classes
The standard library defines a hierarchy rooted at std::exception (declared in <exception>), with a single virtual method, what(), that returns a const char* describing the error. Standard library code throws well-known derived types -- std::runtime_error and std::logic_error (and their further subclasses like std::out_of_range, std::invalid_argument, and std::overflow_error) are the most common -- and your own code should generally derive custom exception types from one of these rather than from std::exception directly, since the *_error classes already provide a working constructor and what() implementation.
InsufficientFundsError builds its human-readable message once, in the constructor, and hands it to std::runtime_error's constructor, so what() works automatically via inherited behavior. It also stores the raw numbers separately as requested_/available_ so a catch block that needs the actual figures (not just a formatted string) can retrieve them through dedicated accessors -- something a plain std::runtime_error thrown with only a message string couldn't offer.
#include <iostream>
#include <stdexcept>
#include <string>
class InsufficientFundsError : public std::runtime_error
{
public:
InsufficientFundsError(double requested, double available)
: std::runtime_error(
"insufficient funds: requested " + std::to_string(requested) +
", available " + std::to_string(available)),
requested_(requested),
available_(available)
{
}
double requested() const noexcept { return requested_; }
double available() const noexcept { return available_; }
private:
double requested_;
double available_;
};
void withdraw(double balance, double amount)
{
if (amount > balance)
throw InsufficientFundsError(amount, balance);
std::cout << "Withdrew " << amount << "\n";
}
int main()
{
try
{
withdraw(100.0, 250.0);
}
catch (const InsufficientFundsError &e)
{
std::cout << e.what() << "\n";
std::cout << "Short by: " << (e.requested() - e.available()) << "\n";
}
return 0;
}noexcept
The noexcept specifier on a function is a promise: this function will not throw. It's not just documentation -- if a function marked noexcept does throw anyway, the runtime calls std::terminate() immediately, without even attempting stack unwinding or looking for a matching catch. That's a much harsher outcome than an ordinary uncaught exception, so noexcept should only be applied to functions you're certain can't throw (or where you genuinely want the program to abort if they somehow do).
Marking a move constructor noexcept is more than a style choice: std::vector and friends specifically check whether a type's move constructor is noexcept before deciding whether reallocation can move elements or must fall back to copying them. Moving raw pointers around like this genuinely cannot throw, so noexcept here is both accurate and consequential for performance.
Compile-time code can also query noexcept-ness with the noexcept operator (a different use of the same keyword): noexcept(someExpression) evaluates to a bool at compile time, without actually running someExpression, telling you whether it's guaranteed not to throw. This is how generic library code like std::vector's move logic decides its strategy without knowing your specific type in advance.
#include <vector>
class Buffer
{
public:
Buffer(Buffer &&other) noexcept
: data_(other.data_), size_(other.size_)
{
other.data_ = nullptr;
other.size_ = 0;
}
private:
int *data_;
std::size_t size_;
};RAII and Exceptions
Stack unwinding only cleans up automatic (stack-allocated) objects by calling their destructors -- it has no idea that a raw pointer variable holds heap memory that needs delete, or that an int variable holds a file descriptor that needs closing. RAII (Resource Acquisition Is Initialization) is what bridges that gap: wrap every resource in an object whose destructor releases it, and stack unwinding's automatic destructor calls become automatic resource cleanup too, exception or no exception.
processLeaky's delete[] buffer; is only reached on the success path -- when shouldFail is true, the throw skips straight past it and the 1000 ints stay allocated forever. processSafe holds the same memory in a std::unique_ptr<int[]> instead: unique_ptr is a stack object, so stack unwinding destroys it exactly like the Guard objects earlier, and its destructor calls delete[] regardless of whether the function returns normally or throws.
This is the real reason C++ code leans so heavily on RAII types -- std::unique_ptr, std::shared_ptr, std::lock_guard, std::vector, std::fstream -- rather than manual new/delete or lock()/unlock() pairs. It isn't merely a style preference; it's what makes exception-thrown code correct instead of leak-prone, because destructors are the one thing guaranteed to run during unwinding.
#include <iostream>
#include <memory>
#include <stdexcept>
void processLeaky(bool shouldFail)
{
int *buffer = new int[1000];
if (shouldFail)
throw std::runtime_error("processing failed"); // buffer leaked -- delete[] never runs
delete[] buffer;
}
void processSafe(bool shouldFail)
{
auto buffer = std::make_unique<int[]>(1000);
if (shouldFail)
throw std::runtime_error("processing failed"); // buffer's destructor still runs during unwinding
}Exception Safety Guarantees (A Preview)
Once RAII guarantees resources get released, the next question is what state your objects are left in after an exception. C++ classifies this into a few standard guarantee levels: the basic guarantee (no leaks, invariants hold, but exact contents are otherwise unspecified after the exception), the strong guarantee (the operation either fully succeeds or has no visible effect at all, as if it never started), and the no-throw guarantee (the operation is certified not to throw, ever). Choosing and documenting which guarantee each of your functions provides is its own discipline -- the next topic on this site, Exception Safety, works through all three in depth, along with the copy-and-swap idiom used to achieve the strong guarantee.
Exceptions vs Error Codes
Exceptions and error codes both communicate failure, but they trade off differently. Exceptions can't be silently ignored the way a return value can, they carry rich data (a whole object, not just an integer or enum), and they unwind cleanly through arbitrarily many stack frames without every intermediate function needing to check and forward anything. Error codes are explicit at every call site, have no runtime cost when nothing goes wrong beyond an ordinary conditional check, and work in environments (some embedded and real-time systems) where exceptions are unavailable or disallowed outright.
Both signatures express the same operation. The error-code version compiles fine even if a caller never checks the bool it returns; the exception version forces every caller to either handle a thrown exception or let it propagate -- there's no third option where the failure just disappears.
A reasonable rule of thumb: reserve exceptions for exceptional, rare conditions -- ones you don't expect to happen in normal, correct operation (a corrupt file, a network drop, memory exhaustion) -- and use ordinary return values (including std::optional or, in C++23, std::expected) for expected, routine outcomes like "this key wasn't found in the map" or "this string didn't parse as a number." Using exceptions for routine control flow tends to make code both slower and harder to read than a plain if check would.
// Error-code style: every caller must remember to check
bool readConfig(const std::string &path, Config &out);
// Exception style: failure can't be silently skipped
Config readConfig(const std::string &path); // throws on failureEmbedded and Real-Time Considerations
Modern C++ implementations use a zero-cost exception model: as long as no exception is actually thrown, there's no runtime overhead at all on the normal execution path -- no per-call checking, no extra branches. The cost shows up only when an exception is actually thrown, in the form of the unwinding machinery walking the stack and the (often nontrivial) size of the tables the compiler generates to support that unwinding. On memory-constrained embedded targets, that table overhead -- plus the difficulty of proving worst-case timing when a throw can happen at unpredictable points -- leads many embedded and safety-critical codebases (aerospace, automotive, some real-time operating systems) to compile with exceptions disabled entirely, using a flag like -fno-exceptions, and rely on error codes or types like std::expected instead.
If you're targeting an embedded platform, check the build flags before designing an API around throw -- code that throws will fail to compile (or link) at all under -fno-exceptions, so the choice between exceptions and error codes for a shared library often isn't a style preference but a hard platform constraint decided before a line of application code is written.
Why can a return-code error scheme fail silently in a way that exceptions cannot?
A return code is just a value -- if a caller never checks it (forgets to, or the code path doesn't require it), the failure information is simply discarded and execution continues as if nothing went wrong. An exception, once thrown, forces the program to either be caught by a handler somewhere on the call stack or terminate the program; there is no code path where a thrown exception is simply ignored the way an unchecked return value can be.
During stack unwinding, which objects get destroyed, and in what order?
Only automatic (stack-allocated) objects that were fully constructed in each stack frame between the throw and the matching catch are destroyed. They are destroyed in the exact reverse order of their construction -- the most recently constructed object in the innermost frame is destroyed first, working back up toward the frame containing the try block.
Why must more specific (derived) exception types be listed before more general (base) exception types when using multiple catch clauses?
catch clauses are tried top to bottom, and the first type that matches -- including a base class matching a derived object -- wins. If a base class catch (like std::exception) comes before a derived class catch (like std::out_of_range), the base clause will catch the derived exception first, making the more specific clause unreachable dead code.
What happens if a function marked noexcept throws an exception anyway, and how is that different from an ordinary uncaught exception?
The runtime calls std::terminate() immediately -- it does not attempt stack unwinding or search for a matching catch handler at all, even if one exists further up the call stack. This is stricter than an ordinary uncaught exception, which does at least unwind the stack looking for a handler before giving up; noexcept violations skip that search entirely.
Why does RAII matter specifically for exception safety, beyond general code tidiness?
Stack unwinding only guarantees that destructors of automatic objects run -- it has no other mechanism for cleanup. A resource managed with a raw handle (a raw new'd pointer, a manually opened file descriptor) has no destructor to call, so if an exception is thrown before the matching manual cleanup line executes, that resource leaks. Wrapping the resource in an RAII type gives it a destructor that unwinding will call automatically, so cleanup happens whether the function returns normally or an exception propagates through it.
What does this program print, in order, and why does the destruction order matter here?
It prints: 'ctor 2', 'ctor 1', 'dtor 1', 'dtor 2', 'caught: oops'. outer() constructs b (id 2) first, then calls inner(), which constructs a (id 1) and throws. Stack unwinding then destroys locals in the reverse order they were constructed -- a (the more recently constructed, innermost object) is destroyed before b -- before control finally reaches main()'s catch block.
Write a custom exception class NegativeValueError deriving from std::invalid_argument that stores the offending value, and a function requirePositive(int value) that throws it when value is negative. Demonstrate catching it in main() and printing both what() and the stored value.
NegativeValueError inherits std::invalid_argument's constructor and what() implementation by passing a formatted message to it, while also keeping the raw int around in a private member so callers can retrieve the exact offending value through value() rather than having to parse it back out of the message string. requirePositive(5) succeeds and prints 'OK: 5'; requirePositive(-3) throws, and the catch block prints both the formatted message and the stored value 'Rejected: negative value not allowed: -3 (value = -3)'.
Write a function safeDivide(int a, int b) marked noexcept that never actually throws, using it to demonstrate how noexcept lets a function report a division-by-zero condition without throwing (returning std::optional<int> instead), and show it being called safely in main().
safeDivide can genuinely be marked noexcept because every path through it -- returning std::nullopt or returning an int wrapped in std::optional -- is itself non-throwing; there's no throw statement and no call to anything that could throw. This shows the alternative to exceptions for a routine, expected failure mode: 10/2 prints 'Result: 5', and 10/0 prints 'Division by zero avoided, no exception thrown' instead of unwinding the stack.
Write a program with a class FileHandle that wraps a raw resource (simulate it with a bool 'open' flag and print statements instead of a real file) using RAII, and a function that opens a FileHandle, does some work that throws partway through, and show that the destructor still runs and 'closes' the handle even though an exception was thrown.
The output is: 'Opening report.txt', 'Working on report.txt', 'Closing report.txt', 'Caught: processing error'. Even though the throw happens before 'Finished report.txt' can print, FileHandle's destructor still runs during stack unwinding because handle is a stack object -- this is exactly why RAII (not manual open/close pairs) is the standard way to guarantee resource cleanup in the presence of exceptions.
#include <iostream>
struct Loud
{
int id;
explicit Loud(int i) : id(i) { std::cout << "ctor " << id << "\n"; }
~Loud() { std::cout << "dtor " << id << "\n"; }
};
void inner()
{
Loud a(1);
throw std::runtime_error("oops");
}
void outer()
{
Loud b(2);
inner();
}
int main()
{
try { outer(); }
catch (const std::exception &e) { std::cout << "caught: " << e.what() << "\n"; }
}