An exception is an abnormal condition that interrupts the normal flow of a program: a product not found, an order total that overflows, a payment record that can't be loaded. C++ provides a dedicated mechanism for signalling these conditions and handling them somewhere up the call stack, separate from the values your functions return. This lesson covers what exceptions are, why they exist, and the high-level shape of how they work. Later lessons in this section cover the syntax, the standard exception hierarchy, custom exception classes, and the runtime mechanics.
An exception is a value (usually an object) that your code creates to signal that something has gone wrong and the function can't continue from here. Once that value is created, normal execution stops at that point. The program walks back up the chain of function calls looking for code that knows how to handle this kind of problem. When it finds one, control jumps there. If it doesn't find one, the program terminates.
An exception is a typed value plus a control-flow jump, both bundled into a single language feature.
Consider ordering something from an online store. A user adds three items to a cart, clicks "Place order", and the system tries to charge the card. If the card is declined, the order can't be completed. The checkout function doesn't know whether the user wants to retry, switch to a different card, save the cart for later, or cancel. That decision belongs to code higher up, closer to the user. The checkout function's job is to report the failure clearly, not to guess what should happen next.
Exceptions are the C++ way to report that kind of failure across function boundaries without baking the recovery policy into low-level code.
Three pieces do the real work in that program. The throw creates a std::runtime_error object and stops applyDiscount from returning a meaningless price. The try block marks the region of code where we're prepared to handle a failure. The catch clause is the handler that takes over when the exception bubbles up. Each piece gets a dedicated lesson in this section.
For a long time, C and early C++ used error codes to signal failures. A function returns an integer (or a special value like nullptr or -1) to indicate what happened, and the caller checks the return value before using any result.
That works, sort of. The trouble is that nothing forces the caller to check. The compiler accepts ignored return values and lets execution proceed as if nothing went wrong.
What's wrong with this code?
The caller never inspected the return value. The product wasn't found, so lookupPrice left price untouched, and price was never initialised. The program reads uninitialised memory and prints whatever happened to be on the stack. The compiler issues at most a warning.
Fix using exceptions:
With the exception version, there is no way for main to keep running with garbage in price. If lookupPrice throws, control jumps straight to the catch block and the bad price line never executes. The failure cannot be dropped without notice.
The "forgot to check" bug is the most visible problem with error codes, but it's not the only one. Three patterns make error codes painful.
A function often has one natural return value: the price, the count, the parsed total. Adding an error channel forces you to pick between mixing the value and the status (using sentinels like -1) or splitting them across an out parameter and a status code. Both are awkward.
Neither shape is great. Sentinels conflict with legitimate values (what if the price genuinely is -1.00 as a refund?). Out parameters split the call into two steps and clutter the function signature.
Exceptions keep the return type clean. double lookupPrice(...) returns a price, full stop. Errors travel through a separate channel.
In any non-trivial program, function A calls B calls C calls D. If D fails, every function in between has to forward the error code up, checking it and returning it again at each level. The code gets buried in boilerplate.
The actual work is three lines. The error-forwarding boilerplate is six. With exceptions, the same routine reads like a straight description of the happy path, and the failure handling sits in one place at the boundary where the error can be dealt with.
When a function returns early because of an error, every resource it acquired (memory, files, locks) has to be released first. Miss a path and you have a leak. Exceptions, combined with C++'s destructors and RAII, handle this automatically: when control leaves a scope (whether by return, by throw, or by reaching the end), every local object's destructor runs.
Exceptions are not free. They are cheap on the success path and expensive on the throw path. A later section covers the cost in detail.
When writing code that throws exceptions, it helps to distinguish two broad categories of "things that went wrong." The standard library uses this split when organising its built-in exception types, but the categories are useful even before learning the type hierarchy.
Logic errors are bugs. They represent situations the programmer should have prevented: an index out of bounds, a null pointer dereferenced, an invariant violated. In principle, you could have caught these by reading the code carefully. They indicate a defect in the program, not a problem with the outside world.
Examples in an e-commerce setting:
Order object with an empty customer ID.Runtime errors are conditions that depend on the world outside the program. The code itself may be correct, but something external didn't cooperate: the database is unreachable, the user typed a malformed address, the file disappeared between checks. Code review cannot rule these out, because they're not bugs.
Examples in the same setting:
The diagram lines up two families of failure. Logic errors say "the program shouldn't have reached this state." Runtime errors say "the program reached a state it can't handle on its own." Both are reported through exceptions, but they call for different responses. Logic errors usually mean fix the code and ship a new build. Runtime errors usually mean surface the problem to the user, try again, fall back to a different path, or roll back.
The standard library has two corresponding base classes, std::logic_error and std::runtime_error, both inheriting from std::exception. They come with subtypes like std::invalid_argument, std::out_of_range, and std::overflow_error.
The exception mechanism in C++ comes down to three steps.
throw to launch it. Execution of the current function stops at that point. Anything after throw in this function is skipped.catch handler that matches the exception's type.try block.The diagram traces one exception's journey. main calls placeOrder, which calls chargeCard, which calls contactBank. contactBank throws. The runtime then walks back through every function on the way down, cleaning up local objects as it goes, until it finds the catch block in main. The bank call site, the charge call site, and the place-order call site all get skipped over without any explicit error-handling code at any of those levels.
The same idea in code:
"Card charged" and "Order placed" never print. The exception interrupted both functions on the way up the stack. The handler in main runs once, and afterwards the program goes on to its final line.
Exceptions are designed for exceptional conditions: states the function meets but can't recover from on its own, that are rare relative to the function's normal use, and that benefit from being reported far up the call chain. Choosing whether to throw is a design decision, and the answer isn't always "yes."
Throw an exception when:
lookupPrice returns a price; if it can't find one, it can't return anything meaningful, and an exception is appropriate.The constructor of an Order object can't return an error code, because constructors don't return values. If the constructor receives an empty customer ID, throwing is the only sensible way to refuse construction.
The constructor rejects invalid input by throwing, and the caller catches the problem at a level where it knows how to react. There's no "half-constructed" Order floating around. Either the object is fully valid or it doesn't exist at all.
Exceptions are a sharp tool, and sharp tools cut the wrong things if used everywhere. Three patterns make exceptions the wrong answer.
Don't use exceptions for routine control flow. If a function has a "normal" outcome and a "not found" outcome, and both happen all the time, neither is exceptional. Looking up a product by ID in a search bar is a good example. Many user-typed searches won't match anything; that's not a failure, it's what the function does. Use a return value (std::optional<Product> works well) and reserve exceptions for the cases that interrupt the program's normal operation.
A "missing product" here is routine, so throwing would be misleading. std::optional describes what the function returns: maybe a price, maybe nothing.
Don't use exceptions for input validation in tight loops. Parsing a million product records where a few percent are malformed, and treating each malformed record as exceptional, means paying the throw cost on every bad row. That's a real performance hit. A return value or a status field on the record is cheaper.
Don't throw from destructors. A destructor is called during stack unwinding, and if a second exception is thrown while another one is already in flight, the program calls std::terminate and dies. Advanced techniques exist to work around this, but the safest rule is: destructors should never throw.
On modern compilers, an exception that is never thrown costs almost nothing on the success path. A thrown exception is much more expensive than a normal return, often by a factor of 10x to 1000x depending on stack depth and compiler. Use exceptions for the rare path, not the common one.
The following e-commerce scenario uses everything covered so far: a constructor that throws on bad input, a function that throws on a runtime failure, and a single catch clause at the top that handles both.
The first cart total prints normally. The second emplace_back throws inside the CartItem constructor, the exception travels up through emplace_back and out of the try block, the partially-built cart's local objects get cleaned up automatically, and the handler in main reports the problem. The "Updated cart total" line never runs. The program doesn't crash, and it doesn't continue with bad data either.
This is the shape most exception-using code takes: low-level functions throw when they meet a state they can't handle, a few outer layers wrap their work in a try block, and a single catch reports the failure to the right audience.
This lesson is the high-level overview. The rest of the section fills in the mechanics.
| Lesson | Topic |
|---|---|
| try, catch & throw | try, catch, and throw syntax in detail |
| Multiple Catch Blocks | Multiple catch handlers, catch(...), and how matching works |
| Standard Exceptions | The std::exception hierarchy: runtime_error, logic_error, and friends |
| Custom Exception Classes | Defining your own exception classes |
| noexcept Specification | noexcept and why marking functions as non-throwing matters |
| Stack Unwinding | Stack unwinding mechanics in depth |
| Exception Best Practices | Exception safety guarantees and patterns for writing safe code |
After completing the section, you will know how to throw and catch, how to design APIs that use exceptions well, what to mark noexcept, and how to write code that remains in a valid state even when an exception passes through it.
10 quizzes