Week 7

Exception Handling

Scott Meyers headshot.

One of the most influential authors in the C++ world is Scott Meyers. His Effective C++ series is the gold standard for learning how to use C++ correctly. He wrote:

"Forty years ago, goto-laden code was considered perfectly good practice. Now we strive to write structured control flows. Twenty years ago, globally accessible data was considered perfectly good practice. Now we strive to encapsulate data. Ten years ago, writing functions without thinking about the impact of exceptions was considered good practice. Now we strive to write exception-safe code.

Time goes on. We live. We learn."

– Scott Meyers, author of Effective C++ and one of the leading experts on C++. [Mey05]

In a perfect world, users would never mistype a URL, programs would have no bugs, disks would never fill up, and your Wi-Fi would never go down. We, however, don't live in a perfect world. You may wake up, planning to head off for school just like any other day, but sometimes, something unexpected appears at your front door. A Polar bear at the door of an igloo.

Fragile code ignores that possibility, but robust code plans for any eventuality.

Week 7

Throwing Exceptions

The C++ exception handling feature allows programs to deal with real life circumstances. The system is broken into three parts:

  • try blocks
  • catch blocks
  • throw statements
  • The running man icon.

Let's start by looking at throw statements, so we can finish up our functions. Click on the running man to open the sample program we've been working on in Replit, Fork it, and we'll continue by learning how to apply exception handling.

When a function encounters a situation from which it cannot recover–for example, a call to stoi("twelve")—you can report the error by using the throw keyword to notify the nearest appropriate error-handling code that something has gone wrong.

istringstream in(str);
int result = 0;
if (in >> result) return result;
throw … // signal error at this point using throw

Inside stoi(), if the read succeeds, return the result. If the read fails, jump out of the function, looking for the closest error handler. You do that with throw.

Like return, throw accepts a single parameter, an object which provides information about the error which occurred.

Week 7

What Should You Throw?

Unlike Java, in C++, it is legal to throw any kind of object, not just members of an exception class hierarchy. So, in C++, all of these are legal:

if (len < 3) throw "Too short"s; // throw a string
if (a > b) throw 42;    // throw an integer error code
if (b < c) throw 3.5;   // throw a double
The C++ standard library exception classes.

The question is, though, what should stoi() throw when an error occurs? The library documentation says that the function throws an invalid_argument exception.

The header file, <stdexcept> defines this and several other classes that let us specify what specific error triggered the exception, similar to the Exception class hierarchy from the Java Class Libraries. (Click the image to enlarge it.)

The invalid_argument exception is ideal because

  • its constructor takes a string argument, useful for error messages.
  • it has a member function what() that returns what the error was
  • Include <stdexcept>, and rewrite the throw statement like this:

    throw invalid_argument(str + " not an int.");
Week 7

The try and catch Blocks

To handle and recover from errors, you need a combination of try and catch blocks. A try block is simply a block of code where runtime errors might occur; write the keyword try, and then surround the appropriate code in a pair of curly braces, like this:

string str;
cin >> str;
try { 
    int a = stoi(str);
    cout << "a = " << a << endl;  // skipped if exception thrown
}
catch (const invalid_argument& e) { 
    cerr << e.what() << endl;     // if exception thrown
{

There are three things to note here.

  1. If the user enters "one" then stoi() will throw an exception, and control immediately breaks out of the try block and jumps to the catch block, skipping the line that prints a. We're guaranteed that the rest of the code in the try block will not execute, preventing error cascades.
  2. If an exception is thrown and caught, control does not return to the try block. Instead, control resumes directly following the try/catch pair.
  3. Catch exception classes from the standard library by const reference. This avoids making copies and enables polymorphism. You will get a compiler warning if you don't do this.

If no error occurs, then all of the code inside the try block executes as normal, and the subsequent catch block is ignored. The function what() will return the string used to construct the exception object. Here, its used to print the error message in the catch block.

Week 7

Other catch Blocks

If your function may throw more than one exception, add cascading catch blocks following the try block, each designed to handle a different type of exception, like this:

try {
    int n = stoi(str);    // may throw an invalid_argument exception
    int x = str.at(5);    // may throw an out_of_range exception
    // ... other statements that may throw exceptions
}
catch (const invalid_argument& e) {
    // handle errors from stoi
}
case (const out_of_range& e) {
    // handle errors from at()
}
case (...) {
    // handle any exceptions not previously caught
}

The last block, with the ... in the argument list is the catch all handler. It catchesany exceptions thrown in the try block, not previously caught. The catch all hander only catches thrown exceptions, not other errors like segmentation faults or operating system traps or signals such as those caused by dividing by zero. Code jumps to only one of the catch blocks shown here. If no exceptions are thrown, then no catch blocks are entered.

Finish the Sample

After adding try-catch to main(), print an error message inside the catch block. Use cerr, print the word "Error: " and then call e.what() like this:

catch (const invalid_argument& e) {
    cerr << "Error: " << e.what() << endl;
}
cout << "--program done--" << endl;

Now your program should work the same whether compiled with C++17 or C++98 (even if the error messages differ between versions.)