Exception Handling

Recognizing and handling errors is an extremely important topic in real-world programming. There are a number of general approaches to error handling, and here we will only consider exceptions.

Throwing Exceptions

In C++, an exception is an object that represents an error, or some sort of exceptional situation. C++ provides a number of language features for dealing with them.

The throw statement is used to manually throw an exception (some programmers say “raise” an exception instead “throw”). For example, here is a variation of the cmpt::error function:

void error(const string& message)
{
    throw std::runtime_error(message);
    cout << "This line is never executed!\n";
}

runtime_error is a standard pre-defined class whose objects are exceptions. The expression std::runtime_error(message) creates a new runtime_error object, and throw causes it to be immediately “thrown” out of the function, skipping any statements that come afterwards.

Suppose we call error like this:

void error_test1() {
    cout << "About to call error ...\n";
    error("testing 1, 2 ,3");
    cout << "... error called!\n";
}

This is what happens when error_test1 runs:

About to call error ...
terminate called after throwing an instance of 'std::runtime_error'
  what():  testing 1, 2 ,3

Notice that the cout statement in error is not called, and the cout statement in error_test1 is also not called. That’s because throw breaks the normal sequence of flow in C++, immediately jumping out a function.

Such jumping can cause serious problems. Consider this example:

void error_test2() {
    int* arr = new int[10];

    error("testing 1, 2 ,3");

    delete[] arr;  // oops: memory leak!
}

An exception has been thrown before delete[] is called, which causes a memory leak! In practice, it can be very hard to know when exceptions might be thrown, and so this can be a source of very subtle errors.

Catching Exceptions

An uncaught exception will crash your program. Since crashing is never desirable in real-life programs, we need some way to handle exceptions. C++ lets you catch exceptions using try/catch blocks. For example:

void error_test3() {
    int* arr = new int[10];

    try {
        error("testing 1, 2 ,3");
        delete[] arr;  // not called if there is an exception thrown above
    } catch (std::runtime_error e) {
        cout << "Caught a runtime exception thrown by error: "
             << e.what() << "\n";
        delete[] arr;  // clean up memory
    }
    cout << "error_test3 finished\n";
}

In this example, if any statement inside the try part of the block throws an exception of type runtime_error, the flow of control will immediately jump to the catch statement and run its body. The cout statement at the end is outside both the try part and the catch part, and so it will always be executed.

We could have written it like this:

void error_test4() {
    int* arr = new int[10];

    try {
        error("testing 1, 2 ,3");
    } catch (std::runtime_error e) {
        cout << "Caught a runtime exception thrown by error: "
             << e.what() << "\n";
    }

    delete[] arr;  // clean up memory
}

Now there is only one call to delete which will be called whether or not an exception is thrown in the body of try.

This catch statement only catches exceptions of type runtime_error. Determining all possible exceptions a block of code might throw can be extremely difficult, even impossible. For example, it is possible (though unlikely) that cout could throw an exception.

If you want to catch all exceptions of any type, you can do this:

void error_test5() {
    int* arr = new int[10];

    try {
        error("testing 1, 2 ,3");
    } catch (...) {  // ... catches all exceptions
        cout << "Caught some exception thrown by error\n";
    }

    delete[] arr;  // clean up memory
}

The catch (...) syntax catches all exceptions that might thrown in the try block, even ones we are not aware of.

A try block can have multiple catch clauses, one for each different type of exception it wants to handle. For example:

void error_test6() {
    int* arr = new int[10];

    try {
        error("testing 1, 2 ,3");
    } catch (std::runtime_error e) {
        cout << "Caught a runtime exception thrown by error: "
             << e.what() << "\n";
    } catch (...) {
        cout << "Caught some unknown exception thrown by error\n";
    }

    delete[] arr;  // clean up memory
}

It is still possible for this code could to throw, but not catch, an exception. The problem is with new: if there is not enough memory to create the array, then new throws bad_alloc. So to be completely sure we catch all exceptions, we should put the call to new inside the the try:

void error_test7() {
    int* arr = nullptr;

    try {
        arr = new int[10];
        error("testing 1, 2 ,3");
    } catch (std::runtime_error e) {
        cout << "Caught a runtime exception thrown by error: "
             << e.what() << "\n";
    } catch (std::bad_alloc e) {
        cout << "bad allocation: " << e.what() << "\n";
    } catch (...) {
        cout << "Caught some other exception\n";
    }

    delete[] arr;  // clean up memory
}

Now delete[] is the only statement outside of the try block. What if it throws an exception? Fortunately, it never will: the rules of C++ explcitly promise that delete and delete[] will never throw an exception.

Note

Aside: Modern C++ offers another way of dealing with these sort of memory leaks: smart pointers. A smart pointer is an object that acts very much like a regular “raw” pointer (such as int*), except it has a destructor that is guaranteed to always correctly delete memory. These are extremely useful in practice, although they do require learning new ways of using pointers. For example, the standard unique_ptr<T> is a smart pointer that points to an object of type T. It is unique in the sense that it guarantees that it is the only pointer pointing to the object, which makes it safe to delete. To ensure uniqueness, there are special rules that restrict how unique_ptr<T> can be used. For example, you cannot copy a unique_ptr, because then there would be more than one pointer pointing to the object.

Exceptions and Destructors

In the above examples, we were able to clean up memory because we had access to arr. But we don’t always have direct access to thing that must be deleted. For example, the C++ vector class (probably!) stores a private pointer to an array on the free store. The user of a vector cannot directly access that pointer, and so that presents a problem in code like this where an exception is thrown:

void error_test8() {
    // v has a private pointer to an underlying array on the free store
    vector<int> v = {1, 2, 3, 4, 5};

    try {
        error("testing 1, 2 ,3");
    } catch (...) {
        cout << "Caught some exception\n";
    }
    // Uh oh: What about the underlying array belonging to v?
    //        Has it be de-allocated? Or is there a memory leak?
 }

Happily, this code does not have a memory leak. If an exception is thrown, C++ explicitly guarantees that destructors are properly called, and so the destructor for v is called whether or not an exception is thrown. Since it is possible for the vector<int> constructor to throw an exception (e.g. bad_alloc due to running out of memory), it’s safer to do this:

void error_test9() {
    try {
        vector<int> v = {1, 2, 3, 4, 5};
        error("testing 1, 2 ,3");
    } catch (...) {
        cout << "Caught some exception\n";
    }
    // v's destructor is guaranteed to be called
}

Again, if any exception is thrown by any statement in the try block, it will be caught by catch (...), and vs destructor is guaranteed to always be properly called. This guarantee of calling destructors when exceptions occur is extremely useful in practice.

In general, you should avoid using pointers directly in C++, and instead wrap them inside of a class that calls new in the constructor and delete/delete[] in the destructor.

Note

C++ and C have a standard function called exit(n) that, when called, will immediately terminate the program and return error code n to the operating system. The problem with exit is that it does call destructors, and so it does not promise to properly clean up any resources that were in use when it was called. For example, if you call `exit while your program has 10 files open and is connected to a printer, those connections are just suddenly severed and not shut down properly. This can cause serious for other programs that want to access them.

RAII: Resource Acquisition is Initialization

This approach of using constructors and destructors to properly manage memory (or other resources) is called resource acquisition is initialization, or RAII for short. The idea is simple and useful:

  • put calls to new in constructors
  • put calls to delete/delete[] in destructors; indeed, any code that must be run, even if an an exception occurs, can be put in the destructor

It’s worth noting that some languages, such as Java and Python, have try/catch blocks with a third construct typically called a finally block. In these languages, you put code that mmust run even if an exception occurs inside the finally block. C++ has no finally block, and uses RAII instead.

Exceptions in Constructors and Destructors

Using exceptions inside constructors and destructors present some issues. The two essential things to remember are:

  1. It’s usually okay for a constructor to throw an exception.
  2. It is almost always bad for a destructor to throw an exception that escapes the destructor. Basically, destructors should never throw exceptions.

For more details, see this discussion.

Parsing Example

Consider the following parsing function that returns the sum of the ints in strings like "5 + 3" and "-43 + 12":

// Takes a string of the form "a + b", where a and b are ints, and returns
// their sum.
int eval(const string& s) {
    // find the position of the '+'
    int plus_loc = s.find('+');
    if (plus_loc == string::npos) cmpt::error("eval: + not found");

    // get a and b as their own strings
    string a = s.substr(0, plus_loc);
    string b = s.substr(plus_loc + 1, s.size() - plus_loc);

    // convert a and b to ints using the standard function stoi (string to
    // int)
    int result = stoi(a) + stoi(b);

    return result;
}

It appears to work fine if you pass it well-formed strings, e.g.:

cout << eval("2 + 3");          // 5
cout << eval("25+-3");          // 22
cout << eval(" 8   +  10   ");  // 18

Notice that extra spaces are no problem. The standard stoi function ignores extra spaces at the beginning or end of a string.

But if you pass it a badly-formed string, it crashes, e.g.:

cout << eval(" two + 3");

terminate called after throwing an instance of 'std::invalid_argument'
  what():  stoi

This error says that the program stopped unexpectedly due to an invalid_argument exception thrown by the stoi function. The problem is that stoi can’t convert “two” to an int.

Here’s another example:

cout << eval(" 2 + 39043090300473");

terminate called after throwing an instance of 'std::out_of_range'
  what():  stoi

The error is different here. The message says that stoi threw an exception because the number is bigger than the biggest possible int.

Using try/catch we can catch these exceptions and print friendlier error messages:

void print_safe(const string& s) {
    try {
        int result = eval(s);
        cout << "result = " << result << "\n\n";
    } catch (std::invalid_argument e) {
        cout << "error: one, or both, of the operands of \"" << s << "\"\n"
             << "       is not a valid int\n\n";
    } catch (std::out_of_range e) {
        cout << "error: one, or both, of the operands of \"" << s << "\"\n"
             << "       are outside the range of an int\n\n";
    } catch (...) {
        cout << "error: an unknown error has occurred\n\n";
    }
}

void test_safe() {
    print_safe(" 11  +33");
    print_safe(" two + 3");
    print_safe(" 2 + 39043090300473");
}

An important idea here is that the development of the eval function was done without worrying about exceptions. Catching the exceptions came later, which lets the programmer separate the parsing code from the error handling code. As you can see, the error handling code is rather messy, and there is a lot of it. If this had been mixed-in with the main code for eval, it would likely have been much less readable.

Notice that the exception thrown by cmpt::error("eval: + not found") is not handled properly in print_safe. The try/catch block in print_safe doesn’t explicitly catch the runtime_error exception, and so it gets treated as an unknown error.

There is at least one other issue with the code in eval: how should examples like eval(2.9 + 2) be handled? It turns out that stoi truncates the 2.9, i.e. 2.9 becomes 2. Some programmers might say that’s an error because we said that the eval function works with ints, and so it should throw an error when given a non-int. Other programmers might say that converting it to an int in this way is fine because, after all, that’s how C++ often does things. So, in this case, it’s up to you, the programmer, to decide if eval(2.9 + 2) ought to be an error. If you do decide it is an error, then you probably have to write a new version of stoi that does what you want.

Exception Propagation

Consider this class, and the three functions that follow it:

class Test {
    string name;
public:
    Test(const string& s)
    : name(s)
    {
        cout << "Test(): " << name << " constructed ...\n";
    }

    ~Test() {
        cout << "... ~Test(): destructor for " << name << " called\n";
    }
};

void c() {
    cout << "c() called ...\n";
    Test c1{"c1"};
    cmpt::error("aaahhhhhh!!!");
    Test c2{"c2"};
    cout << "... c() ended normally\n";
}

void b() {
    cout << "b() called ...\n";
    Test b1{"b1"};
    c();
    Test b2{"b2"};
    cout << "... b() ended normally\n";
}

void a() {
    cout << "a() called ...\n";
    Test a1{"a1"};
    b();
    Test a2{"a2"};
    cout << "... a() ended normally\n";
}

int main() {
    try {
        a();
    } catch (...) {
        cout << "caught an exception!\n";
    }
}

Calling a() prints this:

a() called ...
Test(): a1 constructed ...
b() called ...
Test(): b1 constructed ...
c() called ...
Test(): c1 constructed ...
... ~Test(): destructor for c1 called
... ~Test(): destructor for b1 called
... ~Test(): destructor for a1 called
caught an exception!

The call to error inside c() throws an exception that is not caught inside c(). The exception propagates to b() (because it was b() that called c()), giving b() the chance to catch it. But it doesn’t catch the exception, and so the exception propagates to a(), giving it a chance to catch the exception. But it doesn’t, and so the error keeps propagating up through all the functions that were called, eventually hitting main. Finally, in main, the exception is caught by the try/catch. If main didn’t have a try/catch block, then the program would crash.

Every time you call a function C++ records function calls in a region of memory known as the call stack. Just before error is called in c(), the C++ call stack looks like this:

|      |
| c    | <--- top (most recently called function)
| b    |
| a    |
| main | <--- bottom
--------
call stack

When the most recently called function ends, it is “popped” off the top of the call stack, and the program continues executing at the next line in b() (in addition to the function name, the location of the statement to execute next is also stored). But if c() ends due to an exception being thrown, C++ searches down the call stack, starting at c(), to find if there is a try/catch block that handles the exception. As it searches, it pops off the functions, being sure to call the destructors of all local objects in that function.

This process of going down the call stack and calling destructors is called stack unwinding.

Importantly, throwing an exception is not the same as returning a value. Returning a value and throwing an exception are two different flow control mechanisms that work very differently.