Navigation



15.2 — Basic exception handling

In the previous lesson on introduction to exceptions, we talked about how using return codes causes you control flow and error flow to be intermingled, constraining both. Exceptions in C++ are implemented using three keywords that work in conjunction with each other: throw, try, and catch.

Throwing exceptions

We use signals all the time in real life to note that particular events have occurred. For example, during American football, if a player has committed a foul, the referee will throw a flag on the ground and whistle the play dead. A penalty is then assessed and executed. Once the penalty has been taken care of, play generally resumes as normal.

In C++, a throw statement is used to signal that an exception or error case has occurred (think of throwing a penalty flag). Signaling that an exception has occurred is also commonly called raising an exception.

To use a throw statement, simply use the throw keyword, followed by a value of any data type you wish to use to signal that an error has occurred. Typically, this value will be an error code, a description of the problem, or a custom exception class.

Here are some examples:

throw -1; // throw a literal integer value
throw ENUM_INVALID_INDEX; // throw an enum value
throw "Can not take square root of negative number"; // throw a literal char* string
throw dX; // throw a double variable that was previously defined
throw MyException("Fatal Error"); // Throw an object of class MyException

Each of these statements acts a signal that some kind of problem that needs to be handled has occurred.

Looking for exceptions

Throwing exceptions is only one part of the exception handling process. Let’s go back to our American football analogy: once a referee has thrown a penalty flag, what happens next? The players notice that a penalty has occurred and stop play. The normal flow of the football game is disrupted.

In C++, we use the try keyword to define a block of statements (called a try block). The try block acts as an observer, looking for any exceptions that are thrown by statements within the try block.

Here’s an example of an try block:

try
{
    // Statements that may throw exceptions you want to handle now go here
    throw -1;
}

Note that the try block doesn’t define HOW we’re going to handle the exception. It merely tells the program, “Hey, if you see an exception in the following code, grab it!”.

Handling exceptions

Finally, the end of our American football analogy: After the penalty has been called and play has stopped, the referee assesses the penalty and executes it. In other words, the penalty must be handled before normal play can resume.

Actually handling exceptions is the job of the catch block(s). The catch keyword is used to define a block of code (called a catch block) that handles exceptions for a single data type.

Here’s an example of a catch block:

catch (int)
{
    // Handle an exception of type int here
    cerr << "We caught an exception of type int" << endl;
}

Try blocks and catch blocks work together — A try block detects any exceptions that are thrown by statements within the try block, and routes them to the appropriate catch block for handling. A try block must have at least one catch block attached to it, but may have multiple catch blocks listed in sequence:

int main()
{
    try
    {
        // Statements that may throw exceptions you want to handle now go here
        throw -1;
    }
    catch (int)
    {
        // Any exceptions of type int thrown within the above try block get sent here
        cerr << "We caught an exception of type int" << endl;
    }
    catch (double)
    {
        // Any exceptions of type double thrown within the above try block get sent here
        cerr << "We caught an exception of type double" << endl;
    }

    return 0;
}

Putting throw, try, and catch together

Running the above try/catch block would produce the following result:

We caught an exception of type int

A throw statement was used to raise an exception with the value -1, which is of type int. The try block routed the int exception to the catch block that handles exceptions of type int, which then printed the error message.

Exception handling behind the scenes

Let’s talk about what happens behind the scenes in a little more detail. Exception handling is actually quite simple, and the following two paragraphs are all you really need to know about it:

When an exception is raised (using throw), execution of the program immediately jumps to the nearest enclosing try block (propagating up the stack if necessary). If any of the catch handlers attached to the try block handle that type of exception, that handler is executed and the exception is considered handled.

If no appropriate catch handlers exist, execution of the program propagates to the next enclosing try block. If no appropriate catch handlers can be found before the end of the program, the program will fail with an exception error.

That’s really all there is to it. The rest of this chapter will be dedicated to showing examples of these principles at work.

Exceptions are handled immediately

Here’s a short program that demonstrates how exceptions are handled immediately:

int main()
{
    try
    {
        throw 4.5; // throw exception of type double
        cout << "This never prints" << endl;
    }
    catch(double dX) // handle exception of type double
    {
        cerr << "We caught a double of value: " << dX << endl;
    }
}

This program is about as simple as it gets. Here’s what happens: the throw statement is the first statement that gets executed — this causes an exception of type double to be raised. Execution immediately moves to the nearest enclosing try block, which is the only try block in this program. The catch handlers are then checked to see if any handlers matche. Our exception is of type double, so we’re looking for a catch handler of type double. We have one, so it executes.

Consequently, the result of this program is as follows:

We caught a double of value: 4.5

Note that the cout statement never executed, because the exception caused the execution path to change to the exception handler for doubles.

A more realistic example

Let’s take a look at an example that’s not quite so academic:

#include "math.h" // for sqrt() function
using namespace std;

int main()
{
    cout << "Enter a number: ";
    double dX;
    cin >> dX;

    try // Look for exceptions that occur within try block and route to attached catch block(s)
    {
        // If the user entered a negative number, this is an error condition
        if (dX < 0.0)
            throw "Can not take sqrt of negative number"; // throw exception of type char*

        // Otherwise, print the answer
        cout << "The sqrt of " << dX << " is " << sqrt(dX) << endl;
    }
    catch (char* strException) // catch exceptions of type char*
    {
        cerr << "Error: " << strException << endl;
    }
}

In this code, the user is asked to enter a number. If they enter a positive number, the if statement does not execute, no exception is thrown, and the square root of the number is printed. Because no exception is thrown in this case, the code inside the catch block never executes. The result is something like this:

Enter a number: 9
The sqrt of 9 is 3

If the user enters a negative number, we throw an exception of type char*. Because we’re within a try block and a matching exception handler is found, control immediately transfers to the char* exception handler. The result is:

Enter a number: -4
Error: Can not take sqrt of negative number

By now, you should be getting the basic idea behind exceptions. In the next lesson, we’ll do quite a few more examples to show how flexible exceptions are.

Multiple statements within a try block

In the lesson on the need for exceptions, we showed an example of one case where return codes don’t work very well:

    std::ifstream fSetupIni("setup.ini"); // open setup.ini for reading
    if (!fSetupIni)
        return ERROR_OPENING_FILE; // Some enum value indicating error

    // Note that error handling and actual code logic are intermingled

    if (!ReadParameter(fSetupIni, m_nFirstParameter))
        return ERROR_PARAMETER_MISSING; // Some other enum value indicating error
    if (!ReadParameter(fSetupIni, m_nSecondParameter))
        return ERROR_PARAMETER_MISSING; // Some other enum value indicating error
    if (!ReadParameter(fSetupIni, m_nThirdParameter))
        return ERROR_PARAMETER_MISSING; // Some other enum value indicating error

In this code, ReadParameter() is returning a boolean value indicating success or failure. We end up having to check the return code from each call to ReadParameter() to ensure that it succeeded before proceeding. This leads to code that is messy and redundant.

Let’s rewrite this snippet of code using a new version of ReadParameter() throws an int exception on failure instead of returning a boolean value:

    std::ifstream fSetupIni("setup.ini"); // open setup.ini for reading
    if (!fSetupIni)
        return ERROR_OPENING_FILE; // Some enum value indicating error

    // Read parameters and return an error if the parameter is missing
    try
    {
        // Here's all the normal code logic
        m_nFirstParameter = ReadParameter(fSetupIni);
        m_nSecondParameter = ReadParameter(fSetupIni);
        m_nThirdParameter = ReadParameter(fSetupIni);
    }
    catch (int) // Here's the error handling, nicely separated
    {
        return ERROR_PARAMETER_MISSING; // Some other enum value indicating error
    }

Note how much easier this is to read! If any of the calls to ReadParameter() throws an exception, that exception will be caught and routed to the int exception handler, which returns an error enum to the caller.

15.3 — Exceptions, functions, and stack unwinding
Index
15.1 — The need for exceptions

18 comments to 15.2 — Basic exception handling

You must be logged in to post a comment.