AlgoMaster Logo

Exception Basics

High Priority14 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

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.

What an Exception Actually Is

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.

Why Exceptions Exist

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 Cost of Error Codes

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.

Return values get crowded

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.

Errors propagate badly through nested calls

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.

Cleanup is easy to forget

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.

Runtime Errors vs Logic Errors

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:

  • Adding a product with a negative price because of a sign mistake in the input parser.
  • Indexing past the end of a cart array.
  • Constructing an 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:

  • A product lookup fails because the product was deleted by an admin five minutes ago.
  • A payment provider rejects the card.
  • The inventory file is corrupted and won't parse.

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.

Throw, Unwind, Catch

The exception mechanism in C++ comes down to three steps.

  1. Throw. Code creates an exception object and uses throw to launch it. Execution of the current function stops at that point. Anything after throw in this function is skipped.
  2. Unwind. The runtime walks back up the call stack, destroying every local object whose lifetime ends as it leaves each function. This destruction is automatic and runs the proper destructors. The walk continues until it finds a catch handler that matches the exception's type.
  3. Catch. The matching handler takes over. The exception object is passed into it, and the program resumes execution from inside the handler. After the handler runs, the program continues normally below the corresponding 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.

When to Use Exceptions

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:

  • The function cannot fulfill its contract. lookupPrice returns a price; if it can't find one, it can't return anything meaningful, and an exception is appropriate.
  • A precondition was violated and you'd rather surface the problem than silently continue with bad data.
  • The failure is best handled several frames up, not at the call site. A low-level parser doesn't know how to recover from corrupt input; a higher layer might retry, prompt the user, or abort the operation.
  • Resources need to be cleaned up automatically on the failure path, which RAII plus exceptions handle automatically.

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.

When Not to Use Exceptions

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.

A Larger Example

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.

What's Coming in This Section

This lesson is the high-level overview. The rest of the section fills in the mechanics.

LessonTopic
try, catch & throwtry, catch, and throw syntax in detail
Multiple Catch BlocksMultiple catch handlers, catch(...), and how matching works
Standard ExceptionsThe std::exception hierarchy: runtime_error, logic_error, and friends
Custom Exception ClassesDefining your own exception classes
noexcept Specificationnoexcept and why marking functions as non-throwing matters
Stack UnwindingStack unwinding mechanics in depth
Exception Best PracticesException 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.

Quiz

Exception Basics Quiz

10 quizzes