Exception Handling in C++

Exception handling in C++ is the mechanism used to detect a problem, transfer control to an error-handling path, and keep normal logic separate from failure logic. Instead of checking a return code after every step, a function can throw an exception when it cannot continue correctly, and another part of the program can catch that exception and decide what to do next. This makes code easier to organize, especially in larger programs where an error may happen deep inside a call chain.

If you are learning modern C++, exception handling is an important topic because it connects directly to resource management, class design, standard library usage, and writing reliable applications. Many library functions throw exceptions when they fail, so even if you do not design your own exceptions yet, you still need to understand how to catch and handle them correctly.


What Is Exception Handling in C++?

Exception handling is a structured error handling system built into the language. When an unusual or invalid condition occurs, a program can create an exception object and send it using the throw keyword. The runtime then searches for a matching catch block. If it finds one, control moves there. If no matching handler exists, the program terminates.

Normal code stays focused on the success path, and exceptional situations move to dedicated handlers.

In simple terms, C++ exception handling uses three main keywords:

KeywordPurpose
tryMarks a block of code that may produce an exception.
throwCreates and sends an exception object.
catchHandles an exception of a matching type.

Why Exception Handling Matters

  • It separates error handling from normal business logic.
  • It allows errors to travel up the call stack until a suitable handler is found.
  • It works well with constructors, containers, streams, and other standard library components.
  • It supports safer cleanup through destructors and RAII objects.
  • It makes it easier to report meaningful failure information instead of just returning -1 or false.

That said, exceptions should be used for real error conditions, not for ordinary control flow. For example, reaching the next item in a loop is normal flow; failing to open a required configuration file is an error condition.

Basic Syntax of Exception Handling in C++

try
{
    // code that may throw an exception
}
catch(exception_type variable)
{
    // code that handles the exception
}

The try block contains code that might fail. If an exception is thrown inside it, the remaining statements in that block are skipped. Control then moves to the first matching catch block.

Simple Example of try, throw, and catch

#include <iostream>
using namespace std;
int main()
{
    int numerator = 20;
    int denominator = 0;
    try
    {
        if (denominator == 0)
        {
            throw "Division by zero is not allowed";
        }
        cout << "Result = " << numerator / denominator << endl;
    }
    catch (const char* message)
    {
        cout << "Exception caught: " << message << endl;
    }
    cout << "Program continues after exception handling" << endl;
    return 0;
}

Here, the program checks the denominator before division. When the value is zero, it throws a message. The matching catch block receives that message and prints it. After handling the exception, the program continues with the statements after the catch block.

How Control Moves During an Exception

  1. The program enters a try block.
  2. An error condition occurs.
  3. The program executes throw.
  4. The runtime looks for the nearest matching catch block.
  5. If a match is found, that handler runs.
  6. If no match is found in the current function, stack unwinding begins and the search continues in the caller.
  7. If no handler is found anywhere, the program calls std::terminate().

This ability to move outward through calling functions is what makes exceptions powerful. A low-level function can report failure, while a higher-level function can decide whether to retry, log, clean up, or stop the program.

Using Multiple catch Blocks

#include <iostream>
#include <stdexcept>
using namespace std;
int main()
{
    try
    {
        int choice = 2;
        if (choice == 1)
            throw 100;
        else if (choice == 2)
            throw runtime_error("Runtime problem occurred");
        else
            throw 3.14;
    }
    catch (int value)
    {
        cout << "Integer exception: " << value << endl;
    }
    catch (const runtime_error& e)
    {
        cout << "Runtime error: " << e.what() << endl;
    }
    catch (double value)
    {
        cout << "Double exception: " << value << endl;
    }
}

C++ checks catch blocks from top to bottom. The first matching handler runs, and the rest are ignored. That is why the order of handlers matters. More specific exception types should usually appear before more general ones.

The catch(…) Handler

C++ also provides a generic handler written as catch(...). It catches any exception type. This is useful as a last-resort safety net, especially near the top of the program.

try
{
    throw 5.75;
}
catch (...)
{
    cout << "Some exception was caught" << endl;
}

Although it is useful, catch(...) does not tell you what the actual type was. Because of that, it should usually be placed after specific handlers and used only when you truly want a generic fallback.

Standard Exceptions in C++

The C++ standard library defines many exception classes in headers such as <exception> and <stdexcept>. These classes usually provide a what() member function that returns a descriptive message.

Exception ClassTypical Meaning
std::exceptionBase class for many standard exceptions.
std::runtime_errorError detected only while the program is running.
std::logic_errorError caused by incorrect program logic.
std::out_of_rangeIndex or position is outside valid bounds.
std::invalid_argumentAn argument has an invalid value.
std::bad_allocMemory allocation failed.
#include <iostream>
#include <vector>
#include <exception>
using namespace std;
int main()
{
    vector<int> numbers = {10, 20, 30};
    try
    {
        cout << numbers.at(5) << endl;
    }
    catch (const exception& e)
    {
        cout << "Standard exception caught: " << e.what() << endl;
    }
}

In this example, vector::at() checks the index and throws an exception when the requested position is invalid. This is safer than raw unchecked access because the program gets a clear failure path.

Throwing Your Own Exceptions

You can throw built-in values like int or const char*, but in real C++ code it is usually better to throw objects, especially standard exception types or classes derived from them. This gives better structure and clearer messages.

#include <iostream>
#include <exception>
using namespace std;
class InvalidAgeException : public exception
{
public:
    const char* what() const noexcept override
    {
        return "Age cannot be negative";
    }
};
int main()
{
    int age = -4;
    try
    {
        if (age < 0)
        {
            throw InvalidAgeException();
        }
        cout << "Age is valid" << endl;
    }
    catch (const InvalidAgeException& e)
    {
        cout << "Custom exception caught: " << e.what() << endl;
    }
}

Defining custom exception classes is useful when your application has domain-specific rules, such as invalid account state, file format mismatch, or communication timeout.

Catching Exceptions by Reference

In most cases, exceptions should be caught by const reference rather than by value. Catching by reference avoids an extra copy and preserves polymorphic behavior when using inheritance. That is why code such as catch (const exception& e) is preferred over catch (exception e).

Rethrowing an Exception

Sometimes a function wants to perform partial handling, such as logging, and then let the caller continue handling the same exception. In that case, the current exception can be rethrown using throw; without specifying a new object.

#include <iostream>
#include <stdexcept>
using namespace std;
void processData()
{
    try
    {
        throw runtime_error("Database connection failed");
    }
    catch (const exception& e)
    {
        cout << "Logged inside processData(): " << e.what() << endl;
        throw;
    }
}
int main()
{
    try
    {
        processData();
    }
    catch (const exception& e)
    {
        cout << "Handled in main(): " << e.what() << endl;
    }
}

Using plain throw; keeps the original exception object. Writing throw e; creates a new throw operation and may lose some type information in certain inheritance cases, so it is not the same thing.

Stack Unwinding in C++

When an exception leaves a function, local objects created in that function are destroyed automatically before control moves outward. This process is called stack unwinding. It is one of the biggest reasons RAII is so important in C++. If a resource is owned by an object, the destructor can release that resource even when an exception interrupts the normal flow.

#include <iostream>
#include <stdexcept>
using namespace std;
class Demo
{
public:
    Demo(const char* name)
    {
        cout << name << " constructed" << endl;
        label = name;
    }
    ~Demo()
    {
        cout << label << " destroyed" << endl;
    }
private:
    const char* label;
};
void test()
{
    Demo a("Object A");
    Demo b("Object B");
    throw runtime_error("Something failed in test()");
}
int main()
{
    try
    {
        test();
    }
    catch (const exception& e)
    {
        cout << e.what() << endl;
    }
}

Before the exception reaches main(), both local objects inside test() are destroyed in reverse order. That cleanup happens automatically, which is why smart pointers, file stream objects, and container objects are safer than manual resource management.

Exception Handling and noexcept

The noexcept keyword tells the compiler and other programmers that a function is not expected to throw exceptions. It can affect optimization and is especially important in move constructors, move assignment operators, and low-level utility code.

void displayMessage() noexcept
{
    cout << "This function should not throw" << endl;
}

If an exception escapes from a function declared noexcept, the program usually terminates immediately. Because of that, use noexcept only when you are confident that throwing will not happen or when termination is the intended contract.

Exception Handling vs Return Codes

ApproachStrengthLimitation
Return codesSimple and explicit in small programs.Can clutter code because every call must be checked manually.
ExceptionsKeep the success path cleaner and allow propagation to higher levels.Need disciplined design and proper understanding of cleanup and handler ordering.

Both techniques exist in real software. In embedded systems or performance-sensitive codebases, teams sometimes avoid exceptions for policy reasons. In desktop, server, and library code, exceptions are often a natural fit. The important point is to be consistent within the project.

Common Mistakes in Exception Handling

  • Throwing raw strings or integers everywhere instead of structured exception types.
  • Catching exceptions by value instead of by const reference.
  • Using exceptions for normal loop or branch control.
  • Writing a broad catch(...) too early and hiding useful details.
  • Ignoring resource ownership, which leads to leaks when an exception interrupts the function.
  • Throwing from destructors, which can be dangerous during stack unwinding.
What happens if no catch block matches an exception?

If no matching catch block is found in the current function or any caller, the C++ runtime calls std::terminate(), and the program ends abnormally.

Can one try block have multiple catch blocks?

Yes. This is a very common pattern. The runtime checks the catch blocks in order and runs the first one whose type matches the thrown exception.

Should every function use try and catch?

No. Many functions should simply let exceptions propagate upward. Catch an exception where you can actually handle it, recover from it, translate it, or log it meaningfully.

Best Practices for Exception Handling in C++

  • Throw exception objects, not vague numeric codes.
  • Prefer standard exception classes when they already describe the problem.
  • Catch by const reference.
  • Use RAII so cleanup happens automatically during stack unwinding.
  • Keep handlers close to code that can genuinely recover from the error.
  • Add clear exception messages so logs and debugging output are useful.
  • Use noexcept carefully, not casually.

Once you understand these rules, exception handling in C++ becomes much easier to reason about. The syntax is small, but the design impact is large because it influences how functions communicate failure, how resources are owned, and how robust the program remains when something goes wrong.


Continue learning C++ in order
Follow the topic sequence with the previous and next lesson.