Error Handling and Exceptions
Thus far, we have largely ignored the problem of handling errors in a program. We have included REQUIRES clauses in many of our functions, specifying that the behavior is undefined when the requirements are violated. However, for functions that interact with data external to the program such as user input, files, or other forms of I/O, it is unrealistic to assume that the data will always be available and valid. Instead, we need to specify well-defined behavior for what happens when the requirements for such a function are violated.
In writing a complex program, we subdivide it into individual functions for each task. Each function only knows how to handle its own job, and it returns results to its caller. As a result, it generally is not the case that the function that detects an error is equipped to handle it. Consider the following example:
// EFFECTS: Opens the file with the given filename and returns the
// contents as a string.
string read_file_contents(const string &filename) {
ifstream fin(filename);
if (!fin.is_open()) {
??? // file did not open; now what?
}
...
}
The job of read_file_contents()
is to read data from a file. It
doesn’t know anything further about the program, so it isn’t in a
position to determine what should be done in case of an error.
Depending on the program, the right course of action may be to just
print an error message and quit, prompt the user for a new filename,
ignore the given file and keep going, and so on. Furthermore, the
read_file_contents()
function may actually be used in several
different contexts, each of which requires a different form of error
recovery. The only thing that read_file_contents()
should be
responsible for is detecting and conveying that an error occurred, and
a different function further up in the call stack should do what is
required to handle the error.
Thus, we need a mechanism that separates error detection from error handling, as well as a means for notifying a caller that an error occurred. We will look at several different strategies for doing so.
Global Error Codes
A common strategy in the C language is to set the value of a global
variable to an error code, which provides information about the type
of error that occurred. For example, many implementations of C and C++
functions use the errno
global variable to signal errors. The
following is an example that calls the strtod()
standard-library
function to read a double from a C-style string:
#include <cerrno>
#include <cstdlib>
#include <iostream>
using std::cout;
using std::endl;
int main(int argc, char **argv) {
char *end;
errno = 0; // reset errno before call
double number = std::strtod(argv[1], &end);
if (errno == ERANGE) { // check errno after call
cout << "range error" << endl;
} else {
cout << number << endl;
}
}
When given the representation of a number that is out of range of the
double
type, strtod()
sets errno
to ERANGE
:
$ ./main.exe 1e1000
range error
The strategy of setting a global error code can be (pardon the pun)
error-prone. For example, errno
must be set to zero before calling
a standard-library function that uses it, and the caller must also
remember to check its value after the function call returns.
Otherwise, it may get overwritten by some other error down the line,
masking the original error.
Object Error States
Another strategy used by C++ code is to set an error state on an object. This avoids the negatives of a global variable, since each object has its own error state rather than sharing the same global variable. C++ streams use this strategy:
int main(int argc, char **argv) {
std::ifstream input(argv[1]);
if (!input) {
cout << "error opening file" << endl;
}
}
As with a global variable, the user must remember to check the error state after performing an operation that may set it.
Return Error Codes
Return error codes are another strategy used in C and C++ code to
signal the occurrence of an error. In this pattern, a value from
a function’s return type is reserved to indicate that the function
encountered an error. The following is an example of a factorial()
function that uses this strategy:
// EFFECTS: Computes the factorial of the given number. Returns -1
// if n is negative.
int factorial(int n) {
if (n < 0) {
return -1; // error
} else if (n == 0) {
return 1;
} else {
return n * factorial(n - 1);
}
}
As with a global error code or an object state, it is the
responsibility of the caller to check the code to determine whether an
error occurred. Furthermore, a return error code only works if there
is a value that can be reserved to indicate an error. For a function
such as atoi()
, which converts a C-style string to an int, there
is no such value. In fact, atoi()
returns 0 for both the string
"0"
and "hello"
, and the caller cannot tell whether or not the
input was erroneous.
Exceptions
All three strategies above have the problem that the caller of a function must remember to check for an error; neither the compiler nor the runtime detect when the caller fails to do so. Another common issue is that error checking is interleaved with the regular control flow of the caller, as in the following:
int main(int argc, char **argv) {
double number = std::stod(argv[1]);
string input;
cin >> input;
if (!input) {
cout << "couldn't read input" << endl;
} else if (input == "sqrt") {
cout << std::sqrt(number) << endl;
} else if (input == "log") {
cout << std::log(number) << endl;
}
...
}
The control flow for the error case is mixed in with that of non-error
cases, making the code less readable and harder to maintain.
Furthermore, the code above fails to check for errors in sqrt()
or
log()
, which is not immediately clear due to the interleaving of
control flow.
What we want is a mechanism for error handling that:
Separates error detection from error handling, providing a general means for signaling that an error occurred.
Separates the control flow for error handling from that of non-erroneous cases.
Detects when an error is not handled, which by default should cause the program to exit with a meaningful message.
The mechanism provided by C++ and other modern languages is exceptions.
The following is an example of code that uses exceptions:
// Represents an exception in computing a factorial.
class FactorialError {};
// EFFECTS: Computes the factorial of the given number. Throws a
// FactorialError if n is negative.
int factorial(int n) {
if (n < 0) { // error case
throw FactorialError(); // throw an exception
}
if (n == 0) { // non-error case
return 1;
} else {
return n * factorial(n - 1);
}
}
int main(int argc, char **argv) {
try {
int n = std::stoi(argv[1]);
int result = factorial(n);
cout << n << "! = " << result << endl;
} catch (const std::invalid_argument &error) {
cout << "Error converting " << argv[1] << " to an int: "
<< error.what() << endl;
} catch (const FactorialError &error) {
cout << "Error: cannot compute factorial on negative number"
<< endl;
}
}
The individual components of the exception mechanism used above are:
The
FactorialError
class is an exception type, and objects of that type are used to signal an error.When the
factorial()
function detects an error, it throws an exception object using thethrow
keyword. In the example above, the function throws a default-constructedFactorialError
. The standard-librarystoi()
function throws astd::invalid_argument
object when given a string that does not represent an integer.The compiler arranges for the exception to propagate outward until it is handled by a try/catch block. In the example above, the first catch block is executed when a
std::invalid_argument
is thrown, while the second is run when aFactorialError
is thrown.
When an exception is thrown, execution pauses and can only resume at
a catch block. The code that is between the throw and the catch that
handles the exception is entirely skipped. This is a good thing; if
the rest of the code in factorial()
were to run, the function
would try to compute the factorial of a negative number, which would
recurse indefinitely (or at least until either the program runs out of
stack space, or n
reaches the most negative int
, at which
point the subtraction n - 1
would produce undefined behavior).
The result of running this program on different inputs is as follows:
$ ./main.exe 3
3! = 6
$ ./main.exe -1
Error: cannot compute factorial on negative number
$ ./main.exe hello
Error converting hello to an int: stoi: no conversion
The error message in the last example, specifically the part returned
by error.what()
, is implementation-dependent.
Exception Objects
C++ allows an object of any type to be thrown as an exception. However, a proper exception carries information about what caused the error, and using exceptions of different types allows a program to distinguish between different kinds of errors. More specifically, the example above illustrates that a program can perform different actions in response to each exception type.
There are several exceptions defined by the standard library, such as
invalid_argument
or out_of_range
. The standard-library
exceptions all derive from std::exception
, and it is common for
user-defined exceptions to do so as well:
class EmailError : public std::exception {
public:
EmailError(const string &msg_in) : msg(msg_in) {}
const char * what() const override {
return msg.c_str();
}
private:
string msg;
};
A user-defined exception is a class type, so it can carry any
information necessary to pass between the function that detects an
error and the one that handles it. For EmailError
above, the
constructor takes a string and stores it as a member variable,
allowing any message to be specified when creating an EmailError
object:
throw EmailError("Error sending email to: " + address);
The what()
member function is a virtual function defined by
std::exception
, and EmailError
overrides it to return the
message passed to the constructor. We can call it when catching an
exception to obtain the message:
try {
...
} catch (const EmailError &error) {
cout << error.what() << endl;
}
Exception types are often defined in an inheritance hierarchy, with a
derived class representing a more specific kind of error. Deriving
from std::exception
is an example of this, but we can also define
exception types that derive from EmailError
:
class InvalidAddressError : public EmailError {
...
};
class SendFailedError : public EmailError {
...
};
We will shortly see how exception hierarchies interact with try/catch blocks.
Try/Catch Blocks
A try/catch block consists of a try block followed by one or more catch blocks:
try {
int n = std::stoi(argv[1]);
int result = factorial(n);
cout << n << "! = " << result << endl;
} catch (const std::invalid_argument &error) {
cout << "Error converting " << argv[1] << " to an int: "
<< error.what() << endl;
} catch (const FactorialError &error) {
cout << "Error: cannot compute factorial on negative number"
<< endl;
}
The try/catch can only handle exceptions that occur within the try part of the block, including in functions called by code in the try. When a thrown exception propagates to a try block, the compiler checks the corresponding catch blocks in order to see if any of them can handle an exception of the type thrown. Execution is immediately transferred to the first applicable catch block:
The catch parameter is initialized from the exception object. Declaring the parameter as a reference to const avoids making a copy.
The body of the catch block is run.
Assuming the catch block completes normally, execution proceeds past the entire try/catch. The remaining code in the try or in the other catch blocks is skipped.
In matching an exception object to a catch block, C++ takes into account subtype polymorphism – if the dynamic type of the object is derived from the type of a catch parameter, the catch is able handle that object, so the program uses that catch block for the exception. The following is an example:
void grade_submissions() {
vector<string> students = load_roster();
for (const string &name : students) {
try {
auto sub = load_submission(name);
double result = grade(sub);
email_student(name, result);
} catch (const FileError &error) {
cout << "Can't grade: " << name << endl;
} catch (const EmailError &error) {
cout << "Error emailing: " << name << endl;
}
}
}
The first catch block above handles objects of any type derived from
FileError
, while the second handles objects of any type derived
from EmailError
.
C++ also has a “catch-all” that can handle any exception type:
try {
// some code here
} catch (...) {
cout << "Caught an unknown error" << endl;
}
The ...
as a catch parameter enables the catch block to handle any
type of object. However, this should in general be avoided – instead,
a particular try/catch should only catch the specific kinds of errors
that it is able to recover from. For instance, consider the following
implementation of the load_roster()
function:
vector<string> load_roster() {
try {
csvstream csvin("280roster.csv"); // may throw an exception
// Use the stream to load the roster...
} catch (const csvstream_exception &e) {
cout << e.what() << endl;
// return ???
}
}
The code uses the csvstream library to read a spreadsheet in CSV
format. The library may throw a csvstream_exception
object when
opening the file, such as if the file doesn’t exist. However, the
load_roster()
function is not the best place to handle this
exception; as discussed previously, the appropriate error recovery
depends on the application, and it is the caller of load_roster()
that knows what to do in case of an error. Thus, load_roster()
should avoid catching the exception, letting it propagate to its
caller.
Exception Propagation
When an exception is thrown, if the current function is not within a try block, the exception propagates to the function’s caller. Similarly, if the current function is within a try block but there is no associated catch block that can handle an object of the exception’s type, the exception also propagates to the caller.
To handle an exception, the program immediately pauses execution, then looks for an exception handler as follows. The process starts at the statement that throws the exception:
Determine if the current statement is within the try part of a try/catch block in the current function. If not, the function call is terminated, and the process repeats in the caller.
If the execution is within a try, examine the catch blocks in order to find the first one that can handle an exception of the given type. If no matching catch is found, the function call is terminated, and the process repeats in the caller.
If a matching catch block is found, the catch parameter is initialized with the exception object, and the body of the catch immediately runs. Assuming it completes successfully, execution proceeds past the entire try/catch.
As described above, if the code is not within a try block that can handle the exception object, execution returns immediately to the caller, and the program looks for a handler there. The exception propagates outward until a viable handler is found.
If the exception proceeds outward past main()
, the default
behavior is to terminate the program. This ensures that either the
exception is handled, or the program crashes, informing the user that
an error occurred.
Exception Examples
The following is a DriveThru
class that keeps track of menu items
and their prices:
class InvalidOrderException : public std::exception {};
class DriveThru {
public:
// EFFECTS: Adds the given item to the menu.
void add_item(const string &item, double price) {
menu[item] = price;
}
// EFFECTS: Returns the price for the given item. If the item
// doesn't exist, throws an InvalidOrderException.
double get_price(const string &item) const {
auto it = menu.find(item);
if (it != menu.end()) {
return it->second;
}
throw InvalidOrderException();
}
private:
// A map from item names to corresponding prices
map<string, double> menu;
};
The get_price()
member function looks up the price of an item,
returning it if it is on the menu. If not, it throws an exception of
type InvalidOrderException
. We use the find()
member function
of map
to avoid inserting a value-initialized price into the map
when an item does not exist.
The following is a program that reads order items from standard input, computing the total price:
int main() {
DriveThru eats280;
... // add items to eats280
double total = 0;
string item;
while (cin >> item && item != "done") {
try {
total += eats280.get_price(item);
} catch (const InvalidOrderException &e) {
cout << "Sorry, we don't have: " << item << endl;
}
}
cout << "Your total cost is: " << total << endl;
}
The program ignores items that aren’t on the menu, printing a message
indicating that it is not available. In such a case, get_price()
throws an InvalidOrderException
. Since the throw statement is not
within a try in get_price()
, the exception propagates outward to
main()
. The call to get_price()
is within a try, and there
is an associated catch that matches the type of the exception.
Execution proceeds to that catch, which prints an error message. Then
execution continues after the full try/catch, ending an iteration of
the loop and reading in a new item to start the next iteration
As another example, we write a subset of a program that loads and grades student submissions for an assignment. We use the following classes:
// An exception signifying an error when reading a file.
class FileError : std::exception {
...
};
// An exception signifying an error when sending an email.
class EmailError : std::exception {
...
};
// A student submission.
class Submission {
...
public:
// EFFECTS: Grades this submission and returns the result.
double grade();
};
FileError
and EmailError
are exceptions that the program will
use. We saw the definition of EmailError
previously, and
FileError
is similarly defined. A Submission
object represents
a student submission, and we elide the definition here.
The following functions perform individual tasks in the grading program:
// EFFECTS: Emails the given student the given grade. Throws
// EmailError if sending the email fails.
void email_student(const string &name, double grade);
// EFFECTS: Loads the roster from 280roster.csv. Throws
// csvstream_exception if the file cannot be read.
vector<string> load_roster() {
csvstream csvin("280roster.csv"); // may throw an exception
vector<string> roster;
// Use the stream to load the roster...
return roster;
}
// EFFECTS: Loads the submission for the given student. Throws
// FileError if the submission cannot be loaded.
Submission load_submission(const string &name) {
std::ifstream input(name);
if (!input) {
throw FileError();
}
return Submission(input);
}
All three functions throw exceptions when they are unable to perform
their task. The email_student()
function throws an EmailError
if sending fails. The load_roster()
function throws a
csvstream_exception
if reading the roster file fails; in
actuality, the exception will be thrown by the csvstream
constructor, but since load_roster()
allows the exception to
propagate outward, it documents that such an exception may be thrown.
Finally, load_submissions()
throws a FileError
if a submission
file fails to open.
The function that directs the grading process is as follows:
// EFFECTS: Loads and grades all submissions, sending email to each
// student with their result. Throws csvstream_exception
// if the roster cannot be loaded.
void grade_submissions() {
vector<string> students = load_roster();
for (const string &name : students) {
try {
auto sub = load_submission(name);
double result = sub.grade();
email_student(name, result);
} catch (const FileError &e) {
cout << "Can't grade: " << s << endl;
} catch (const EmailError &e) {
cout << e.what() << endl;
}
}
}
This function handles FileError
and EmailError
exceptions by
just printing a message to standard out. The try/catch is within the
for loop so that failure for one student does not prevent grading of
other students. If a csvstream_exception
is thrown by
load_roster()
, it gets propagated to the caller of
grade_submissions()
.
Lastly, we have the main()
function:
int main() {
try {
grade_submissions();
cout << "Grading done!" << endl;
} catch (const csvstream_exception &e) {
cout << "Grading failed! << endl;
cout << e.what() << endl;
return EXIT_FAILURE;
}
}
This function handles a csvstream_exception
by printing a message
to standard out and then returning with a nonzero exit code,
indicating a failure.
We consider a few more small examples to better understand how exceptions are propagated and handled. The examples use the following exception types:
class GoodbyeError {};
class HelloError {};
class Error {
public:
Error(const string &s) : msg(s) {}
const string & get_msg() {
return msg;
}
private:
string msg;
};
Objects of GoodbyeError
will generally be thrown by a
goodbye()
function, while HelloError
objects will be thrown by
hello()
.
The first example is as follows:
void goodbye() {
cout << "goodbye called" << endl;
throw GoodbyeError();
cout << "goodbye returns" << endl;
}
void hello() {
cout << "hello called" << endl;
goodbye();
cout << "hello returns" << endl;
}
int main() {
try {
hello();
cout << "done" << endl;
} catch (const HelloError &he) {
cout << "caught hello" << endl;
} catch (const GoodbyeError &ge) {
cout << "caught goodbye" << endl;
}
cout << "main returns" << endl;
}
In this example, hello()
is called from within a try in
main()
. So if an exception is thrown and propagates to main()
,
the associated catch blocks will attempt to handle the exception.
Within hello()
, the message hello called
is printed, followed
by a call to goodbye()
. The latter prints out goodbye called
and then throws a GoodbyeError
object. Execution immediately
pauses, and the program checks if it is within a try in goodbye()
.
It is not, so it then checks the caller to see if it is currently in a
try there. There isn’t one in hello()
, so the program then checks
for a try in main()
. The execution state is indeed within a try in
main()
, so the program checks the catch blocks, in order, to see
if there is one that can handle a GoodbyeError
. The second catch
can do so, and its code is run, printing caught goodbye
. Execution
then proceeds past the try/catch, so the print of main returns
executes.
Observe that the remaining code in goodbye()
, hello()
, and the
try block in main()
were skipped when handling the exception. The
full output is as follows:
hello called
goodbye called
caught goodbye
main returns
Consider another example:
void goodbye() {
cout << "goodbye called" << endl;
throw GoodbyeError();
cout << "goodbye returns" << endl;
}
void hello() {
cout << "hello called" << endl;
try {
goodbye();
} catch (const GoodbyeError &ge) {
throw HelloError();
} catch (const HelloError &he) {
cout << "caught hello" << endl;
}
cout << "hello returns" << endl;
}
int main() {
try {
hello();
cout << "done" << endl;
} catch (const HelloError &he) {
cout << "caught hello" << endl;
} catch (const GoodbyeError &ge) {
cout << "caught goodbye" << endl;
}
cout << "main returns" << endl;
}
This is the same as the first example, except now the call to
goodbye()
is within a try in hello()
. When the
GoodbyeError
object is thrown, the program determines that it is
not within a try in goodbye()
, so it checks whether it is in a try
in hello()
. It is, so the program checks whether there is a catch
block that can handle a GoodbyeError
. The first catch block can do
so, so its code is run. This throws a HelloError
, so the program
checks whether execution is within a try in hello()
. It is not –
execution is within a catch block, not within a try. So the program
proceeds to the caller, checking whether execution is in a try in
main()
. The call to hello()
is indeed within a try, so the
program examines the catch blocks. The first one handles a
HelloError
, so its body is executed, printing caught hello
.
Then execution proceeds past the try/catch to the print of main
returns
.
Observe that a try/catch can only handle exceptions that are thrown within the try block; it does not deal with exceptions that are thrown from one of its catch blocks, so an outer try/catch must handle such exceptions instead.
hello called
goodbye called
caught hello
main returns
The following example uses the Error
class defined above, which
has a constructor that takes a string:
void goodbye() {
cout << "goodbye called" << endl;
throw Error("bye");
cout << "goodbye returns" << endl;
}
void hello() {
cout << "hello called" << endl;
try {
goodbye();
} catch (const Error &e) {
throw Error("hey");
}
cout << "hello returns" << endl;
}
int main() {
try {
hello();
cout << "done" << endl;
} catch (const Error &e) {
cout << e.get_msg() << endl;
} catch (...) {
cout << "unknown error" << endl;
}
cout << "main returns" << endl;
}
The throw statement in goodbye()
throws an Error
object
constructed with the string "bye"
. There is no try in
goodbye()
, so the program checks whether it is currently within a
try in hello()
. Execution is indeed within the try there, and the
catch block can handle the Error
object. The catch block throws a
different Error
object, initialized with the string "hey"
. As
with the previous example, this throw statement is not within a try in
hello()
, so the program checks for a try in main()
. Both catch
blocks can handle the Error
object; the second is a catch-all that
can handle any exception. However, the program considers the catch
blocks in order, so it is the first one that runs. Its body retrieves
the message from the Error
object, printing out hey
. Then
execution proceeds past the try/catch. The full output is below:
hello called
goodbye called
hey
main returns
One more example is the following:
void goodbye() {
cout << "goodbye called" << endl;
throw GoodbyeError();
cout << "goodbye returns" << endl;
}
void hello() {
cout << "hello called" << endl;
try {
goodbye();
} catch (const Error &e) {
throw Error("hey");
}
cout << "hello returns" << endl;
}
int main() {
try {
hello();
cout << "done" << endl;
} catch (const Error &e) {
cout << e.get_msg();
cout << endl;
} catch (...) {
cout << "unknown error" << endl;
}
cout << "main returns" << endl;
}
Here, the goodbye()
function throws a GoodbyeError
, and the
throw is not within a try in goodbye()
, so the program looks for a
try in hello()
. Execution is within a try there, so the program
examines the catch blocks to see whether one can handle a
GoodbyeError
. The lone catch block cannot, so the program
propagates the exception to the caller and looks for a try in
main()
. The code is within a try in main()
, so the program
examines the catch blocks in order. The first cannot handle a
GoodbyeError
, but the second can, so the latter runs and prints
unknown error
. Execution continues after the try/catch, printing
main returns
. The full result is the following:
hello called
goodbye called
unknown error
main returns
Error Handling vs. Undefined Behavior
The error-detection mechanisms we discussed provide well-defined behavior in case of an error. As such, a function that uses one of these mechanisms should document it, describing when and what kind of error can be generated:
// EFFECTS: Computes the factorial of the given number. Returns 0 if
// n is negative.
int factorial(int n);
// EFFECTS: Emails the given student the given grade. Throws
// EmailError if sending the email fails.
void email_student(const string &name, double grade);
These functions do not have REQUIRES clauses that restrict the input; violating a REQUIRES clause results in undefined behavior, whereas these function produce well-defined behavior for erroneous input.
On the other hand, when code does produce undefined behavior, it cannot be detected through any of the error-handling mechanisms above. For example, dereferencing a null or uninitialized pointer does not necessarily throw an exception, set a global error code, or provide any other form of error detection. It is the programmer’s responsibility to avoid undefined behavior, and there are no constraints on what a C++ implementation can do if a program results in undefined behavior.