Exception Handling in C++ — try, catch, throw and Real-World Patterns
Exception handling in C++ explained with real-world patterns, RAII, stack unwinding, and exception safety.
- C++ exceptions separate error handling from normal logic using try, throw, catch
- Stack unwinding destroys local objects in reverse order, enabling RAII safety
- Always catch by const reference to avoid slicing; throw by value
- Zero-cost model: no overhead on the happy path, only when an exception is thrown
- In production, uncaught exceptions call std::terminate() — always have a top-level catch-all
- Biggest mistake: throwing inside a destructor, which causes immediate termination during stack unwinding
Imagine you're baking a cake and the recipe says 'add eggs' — but you open the fridge and there are none. You don't just freeze and stare at the wall; you handle the situation: maybe you go buy eggs, maybe you make a different cake, or maybe you tell whoever asked for the cake that it can't be done today. Exception handling in C++ works exactly the same way. Your program tries to do something, something unexpected goes wrong, and instead of crashing silently or doing something dangerous, it raises its hand (throws an exception), someone catches that raised hand, and a decision is made about what to do next.
Software breaks. Not because developers are careless — but because the real world is unpredictable. A file your program needs gets deleted by the user. A network call times out at 2 AM. A user types their age as 'banana'. These aren't bugs in your logic; they're edge cases that exist at the boundary between your code and reality. Without a structured way to handle them, your program either crashes loudly with a segfault, or worse, silently produces wrong results. Neither is acceptable in production software.
C++ gives you exception handling — a formal, structured mechanism to separate the 'happy path' code from the 'something went wrong' code. Before exceptions existed, C programs (and early C++) handled errors by returning special integer codes like -1 or NULL. Every caller had to manually check those return values, and if even one layer forgot, the error disappeared silently up the call stack. Exceptions changed that: an error thrown deep in a library function can't be silently ignored — it will unwind the call stack until something explicitly catches it, or terminate the program making the problem impossible to miss.
By the end of this article you'll understand not just the syntax of try, catch, and throw, but WHY they work the way they do, how stack unwinding protects your resources, how to design your own exception hierarchy for real projects, and the exact mistakes that trip up even experienced C++ developers. You'll be able to write exception-safe code and explain the design choices behind it in an interview.
The Core Mechanic — try, throw, and catch in Plain English
The three keywords work as a team. You wrap risky code in a try block — code that might fail. If something goes wrong inside that block, you throw an exception object. Control immediately jumps out of the try block and into the matching catch block. Nothing between the throw site and the catch runs.
This separation is the whole point. Your normal logic stays clean inside the try block. Your error-handling logic lives in catch. They never tangle together with nested if-checks.
The thing you throw can be almost anything — an integer, a string, or (the right approach) an object from a class. In practice, you should always throw objects, not primitives, because objects can carry rich context: a message, an error code, the filename where the error happened. Throwing an int like -1 leaves the catcher with no context about what went wrong.
Here's a concrete example: you're parsing user input. A valid parse produces an int. A failure produces a typed exception that carries the bad input string. The catch block can then include that string in a user-friendly error message.
Stack Unwinding — How C++ Cleans Up After an Exception
When an exception is thrown, C++ systematically destroys every local object between the throw site and the catch block, in reverse order of construction. This process is called stack unwinding.
This is the foundation of RAII (Resource Acquisition Is Initialization). If you use stack-allocated RAII wrappers like std::unique_ptr or std::lock_guard, their destructors run during unwinding even if an exception interrupts your function. This prevents memory leaks and stale locks without requiring manual cleanup code in every catch block.
The key detail: unwinding only applies to fully constructed objects. If an exception is thrown inside a constructor, only subobjects that have completed construction are destroyed. This is why you should never allocate resources with raw pointers in a constructor — use RAII wrappers that handle cleanup automatically.
Exception Safety Guarantees — The Three Levels
When you write a function that may throw, you must decide what guarantees you provide to the caller. C++ defines three levels:
- Basic guarantee: The function will not leak resources or leave invariants broken. After an exception, the object is in a consistent (but possibly unspecified) state.
- Strong guarantee: If an exception is thrown, the program state is rolled back to exactly what it was before the call. The operation either fully succeeds or has no effect — think transactional.
- No-throw guarantee: The function never throws. This is the strictest. Destructors, swap, and move operations should always offer this.
Achieving these guarantees requires careful use of RAII, copy-and-swap idiom, and separating resource acquisition from side effects. In production, aim for strong guarantee on all public API functions, but measure the cost — making everything transactional may require deep copies or extra allocations.
- The strong guarantee = atomic commit: either everything or nothing.
- Copy-and-swap is the go-to pattern for strong guarantee on mutable classes.
- The no-throw guarantee is for destructors, swap, and move operations.
- The basic guarantee is the minimum for all functions; you must at least not leak.
Designing Custom Exception Hierarchies for Production
Throwing built-in std::runtime_error everywhere is better than throwing ints, but it won't scale. For a large codebase you need a domain-specific exception hierarchy rooted at std::exception. This lets callers catch by category (e.g., NetworkError, DatabaseError, ValidationError) and provides rich context.
- Derive from
std::runtime_errororstd::logic_error, not directly fromstd::exception. The base class has no meaningful.what() - Keep the hierarchy shallow — two or three levels deep is enough. Too deep and catching becomes difficult.
- Each exception should carry as much context as possible: error code, operation name, relevant IDs, and a human-readable message.
- Make exception classes non-copyable? No — they must be copyable because they are thrown by value. But avoid deep copies; store strings, not complex objects.
- Mark exception constructors as
noexceptto prevent a second exception during throw.
A well-designed hierarchy turns an opaque stack trace into a structured error that can be logged, retried, or transformed into an HTTP response.
noexcept, Move Semantics, and Performance
The noexcept specifier tells the compiler that a function will never throw. This is not just documentation — it enables critical optimisations, especially in move operations. std::vector uses std::move_if_noexcept to decide whether to move elements during reallocation: if the move constructor is noexcept, elements are moved; otherwise they are copied. Copying can be orders of magnitude slower.
Similarly, standard library containers and algorithms prefer noexcept swap and move. Marking your move operations as noexcept can dramatically improve performance when resizing containers.
But noexcept is a contract. If you lie and the function does throw, std::terminate is called. Only use noexcept on functions you are certain cannot throw (destructors, simple moves, swaps, and trivial accessors). For complex logic that might throw (file I/O, network calls), do not mark noexcept.
In production, audit any function that is marked noexcept but contains a throw (or calls a throwing function). The compiler does not warn you — it calls terminate silently.
noexcept annotation restored performance. Measure before and after.The Midnight Terminate: A Destructor That Threw
rollback() which threw an exception if the transaction was already partially committed. The rollback function was called during stack unwinding from a different exception, causing a second exception — C++ calls std::terminate() immediately.- Destructors must never throw — mark them noexcept, even if they internally might fail.
- Any function called from a destructor (directly or indirectly) must also be noexcept or catch all exceptions.
- Add a top-level catch(...) in
main()to catch every exception and log it before termination.
Key takeaways
const std::exception& e) and throw by value to prevent object slicing and memory leaks.std::exception to ensure interoperability with standard library and third-party handlers.std::optional or std::expected (C++23).noexcept to prevent double-exception termination during stack unwinding.noexcept to enable fast reallocation in standard containers.Common mistakes to avoid
5 patternsThrowing inside a destructor
Catching exceptions by value instead of by const reference
catch (const SpecificException& e). The ampersand prevents slicing and avoids a copy.Using `throw new MyException()` (throwing by pointer)
delete the pointer. The exception object is allocated but never freed if the catcher only handles references.throw MyException(args). The exception object is managed by the C++ runtime and automatically destroyed after the catch.Using exceptions for normal control flow
Not providing a top-level catch-all in main()
catch (const std::exception& e) { log(e.what()); return 1; } catch (...) { log("Unknown exception"); return 1; }. At least log and exit gracefully.Interview Questions on This Topic
What is the 'Zero-cost' exception model and how does it impact the 'happy path' performance of a C++ application?
Frequently Asked Questions
That's C++ Basics. Mark it forged?
5 min read · try the examples if you haven't