Skip to content
Home C / C++ Exception Handling in C++ — try, catch, throw and Real-World Patterns

Exception Handling in C++ — try, catch, throw and Real-World Patterns

Where developers are forged. · Structured learning · Free forever.
📍 Part of: C++ Basics → Topic 11 of 19
Exception handling in C++ explained with real-world patterns, RAII, stack unwinding, and exception safety.
⚙️ Intermediate — basic C / C++ knowledge assumed
In this tutorial, you'll learn
Exception handling in C++ explained with real-world patterns, RAII, stack unwinding, and exception safety.
  • Always catch by const reference (const std::exception& e) and throw by value to prevent object slicing and memory leaks.
  • Stack unwinding ensures that RAII objects are cleaned up automatically, making resource management robust even during failures.
  • Exception hierarchies should be rooted in std::exception to ensure interoperability with standard library and third-party handlers.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer

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.

BasicExceptionDemo.cpp · CPP
12345678910111213141516171819202122232425262728293031
#include <iostream>
#include <stdexcept>
#include <string>

namespace io::thecodeforge::exceptions {

int parseAge(const std::string& rawInput) {
    // std::stoi throws invalid_argument or out_of_range internally
    int age = std::stoi(rawInput);

    if (age < 0 || age > 150) {
        throw std::out_of_range("Age must be 0-150, got: " + rawInput);
    }
    return age;
}

}

int main() {
    try {
        int age = io::thecodeforge::exceptions::parseAge("300");
        std::cout << "Age: " << age << "\n";
    } catch (const std::invalid_argument& e) {
        std::cerr << "Format Error: " << e.what() << "\n";
    } catch (const std::out_of_range& e) {
        std::cerr << "Range Error: " << e.what() << "\n";
    } catch (const std::exception& e) {
        std::cerr << "Generic Error: " << e.what() << "\n";
    }
    return 0;
}
▶ Output
Range Error: Age must be 0-150, got: 300
⚠ Watch Out: catch order matters
Always list more specific exception types BEFORE more general ones. If you put catch(std::exception&) first, it swallows every derived exception type and your specific catch blocks below it will never execute.

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.

StackUnwindingDemo.cpp · CPP
123456789101112131415161718192021222324252627282930313233343536373839
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>

namespace io::thecodeforge::resources {

class DatabaseConnection {
public:
    explicit DatabaseConnection(const std::string& host) : host_(host) {
        std::cout << "[DB] Connected to " << host_ << "\n";
    }
    ~DatabaseConnection() {
        std::cout << "[DB] Connection to " << host_ << " closed cleanly.\n";
    }
    void query(const std::string& sql) {
        if (sql.find("DROP") != std::string::npos) {
            throw std::runtime_error("Forbidden query: " + sql);
        }
    }
private:
    std::string host_;
};

void runTask() {
    auto db = std::make_unique<DatabaseConnection>("prod-db-01");
    db->query("DROP TABLE users"); // Throws
}

}

int main() {
    try {
        io::thecodeforge::resources::runTask();
    } catch (const std::exception& e) {
        std::cerr << "Caught: " << e.what() << "\n";
    }
    return 0;
}
▶ Output
[DB] Connected to prod-db-01
[DB] Connection to prod-db-01 closed cleanly.
Caught: Forbidden query: DROP TABLE users
💡Pro Tip: RAII is your safety net
Notice the destructor ran BEFORE the catch block. Never manage a raw resource without an RAII wrapper. If you find yourself writing 'delete' or 'fclose' manually, you are likely writing exception-unsafe code.
AspectReturn Codes (C-style)C++ Exceptions
Error propagationManual — every caller must check and forwardAutomatic — propagates up the call stack without caller involvement
Forgetting to handleSilent — missed check means error disappearsLoud — uncaught exception calls std::terminate(), crash is obvious
Error contextJust a number (e.g., -1, errno)Rich object with message, type, and extra fields
Performance on successMinimal branching costZero overhead (Zero-cost model) on modern compilers
Resource safetyManual cleanup required before each returnAutomatic via RAII and stack unwinding

🎯 Key Takeaways

  • Always catch by const reference (const std::exception& e) and throw by value to prevent object slicing and memory leaks.
  • Stack unwinding ensures that RAII objects are cleaned up automatically, making resource management robust even during failures.
  • Exception hierarchies should be rooted in std::exception to ensure interoperability with standard library and third-party handlers.
  • Reserve exceptions for truly exceptional events. For logic that fails frequently in normal operation, consider std::optional or std::expected (C++23).

⚠ Common Mistakes to Avoid

    Throwing by pointer instead of by value — `throw new MyException()` leaks memory if the catcher doesn't call delete. Always throw by value: `throw MyException("message")`.

    message")`.

    Catching by value instead of by reference — `catch(std::exception e)` slices the exception, losing derived-class data. Always catch by const reference: `catch(const std::exception& e)`.

    ption& e)`.

    Throwing inside a destructor — If a destructor throws while an exception is already propagating, C++ calls `std::terminate()`. Destructors must be `noexcept`.

    noexcept.

    Using exceptions for flow control — Don't use `throw` to break out of a loop. Exceptions are for exceptional failures, not expected logic branches.

    c branches.

Interview Questions on This Topic

  • QWhat is the 'Zero-cost' exception model and how does it impact the 'happy path' performance of a C++ application?
  • QExplain Object Slicing in the context of catch blocks. Why is catch (const std::exception& e) preferred over catch (std::exception e)?
  • QTrace the Stack Unwinding process. What happens to local objects if an exception is thrown in a constructor's initializer list?
  • QWhat are Exception Safety Guarantees? Describe the difference between the 'Strong' guarantee and the 'No-throw' guarantee.
  • QHow does the noexcept specifier assist the compiler in optimizing move operations within a std::vector?

Frequently Asked Questions

What is the difference between std::runtime_error and std::logic_error in C++?

std::logic_error represents bugs that could have been prevented by the programmer (e.g., out-of-bounds access). std::runtime_error represents conditions outside the program's direct control (e.g., file not found or network timeout).

Can I throw any type in C++, like an int or a string?

Yes, C++ technically allows you to throw any type. However, production-grade code should only throw classes derived from std::exception. This allows catchers to use a consistent interface (.what()) and prevents the 'mystery catch' problem.

Why shouldn't I use catch(...) everywhere?

Catch-all blocks (catch(...)) hide the specific cause of a failure. They are useful at the very top level of an application (like in main()) to prevent a hard crash, but they should usually log the event and then rethrow or exit cleanly rather than continuing in an unknown state.

What is 'Exception Slicing'?

Exception slicing occurs when you catch an exception by value. The compiler creates a new instance of the base class, 'slicing off' all the extra data and polymorphic behavior of the derived exception. This makes it impossible to know the true nature of the error.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousReferences in C++Next →File I/O in C++
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged