University of Michigan at Ann Arbor
Last Edit Date: 04/12/2023
Disclaimer and Term of Use:
We do not guarantee the accuracy and completeness of the summary content. Some of the course material may not be included, and some of the content in the summary may not be correct. You should use this file properly and legally. We are not responsible for any results from using this file
This personal note is adapted from Professor Amir Kamil, Andrew DeOrio, James Juett, Sofia Saleem, and Saquib Razak. Please contact us to delete this file if you think your rights have been violated.
This work is licensed under a Creative Commons Attribution 4.0 International License.
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:
1 #include <cerrno>
2 #include <cstdlib>
3 #include <iostream>
4
5 using std::cout;
6 using std::endl;
7
8 int main(int argc, char **argv) {
9 char *end;
10 errno = 0; // reset errno before call
11 double number = std::strtod(argv[1], &end);
12 if (errno == ERANGE) { // check errno after call
13 cout << "range error" << endl;
14 } else {
15 cout << number << endl;
16 }
17 }
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.
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:
1 int main(int argc, char **argv) {
2 std::ifstream input(argv[1]);
3 if (!input) {
4 cout << "error opening file" << endl;
5 }
6 }
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 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:
1 // EFFECTS: Computes the factorial of the given number. Returns -1
2 // if n is negative.
3 int factorial(int n) {
4 if (n < 0) {
5 return -1; // error
6 } else if (n == 0) {
7 return 1;
8 } else {
9 return n * factorial(n - 1);
10 }
11 }
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.
The exception mechanism introduces an additional control flow path for error handling.
1 class FactorialError { };
2 // Returns n! for non-negative
3 // inputs. Throws an exception
4 // on negative inputs.
5 int factorial(int n) {
6 // Check for error
7 if (n < 0) {
8 throw FactorialError();
9 }
10 ...
11 }
12
13 int main() {
14 int x = askUser();
15 try {
16 int f = factorial(x);
17 if (f < 100) {
18 cout << "Small" << endl;
19 }
20 else {
21 cout << "Larger" << endl;
22 }
23 }
24 catch (const FactorialError &e) {
25 cout << "ERROR" << endl;
26 }
27 }
The language is essentially providing us with a structured way to:
Detect Errors: Create and throw an error-like object called an exception, which contains information about what happened.
Propagate the exception outward from a function to its caller until it is handled.
Handle Errors: Catch the exception in a special block of code that handles the error.
The throw Statement
When a throw statement is encountered, regular control flow ceases.
The program proceeds outward through each scope until an appropriate catch is found.
You can throw any kind of object, but generally we use a class type created to represent a particular kind of error (ex: FactorialError
).
Only one object can ever be thrown at a given time (no juggling allowed).
The try-catch
block
A try block is always matched up with one or more catch blocks.
If an exception is thrown inside a try block, the corresponding catch blocks are examined.
If a catch block matches the type of the exception, the code in that block executes.
If there is no matching catch, the exception continues outward.
Uncaught exception == crash.
Custom Exception Types
The type itself indicates the kind of error.
The thrown object may also carry along extra information.
1 class EmailError : public std::exception {
2 public:
3 EmailError(const string &msg_in) : msg(msg_in) { }
4 const char * what() const override { return msg.c_str(); }
5 private:
6 string msg;
7 };
Note: 1. The best practice is to derive from std::exception
. 2. Override what()
member function to retrieve message.
Remember, always use catch-by-reference (to const
)
try{ ... }
catch (const EmailError &e) {
cout << e.what() << endl;
}
Exceptions and Polymorphism
It is common to define a hierarchy of exception types, which can be caught polymorphically.
class EmailError : public std::exception { ... };
class InvalidAddressError : public EmailError { ... };
class SendFailedError : public EmailError { ... };
void gradeSubmissions() {
vector<string> students = loadRoster();
for (const string &s : students) {
try {
auto sub = loadSubmission(s);
double result = grade(sub);
emailStudent(s, result);
}
catch (const FileError &e) {
cout << "Can't grade: " << s << endl;
}
catch (const EmailError &e) { ... }
}
}
Note: EmailError
catches any kind of EmailError
. Note the catch by reference is necessary for polymorphism.
Multiple Catch Blocks
Catch blocks are tried in order.
The first matching block is used.
At most one catch block will ever be used.
If none match, the exception continues outward.
try {
// Some code that may throw many different kinds of exceptions
}
catch (const InvalidAddressError &e) {
cout << e.getMessage() << endl;
// Also, remove the recipient from our address book
}
catch (const EmailError &e) {
cout << "Error sending mail: << e.getMessage() << endl;
}
catch (const SomeOtherError &e) {
// Do something to handle this specific other error.
}
catch (...) {
cout << "Error occurred!" << endl;
}
Note:
catch (const InvalidAddressError &e)
attempts to match a specific kind of email error.
catch (const EmailError &e)
matches any remaining email errors.
catch (...)
will match anything, but it may not be a good idea to use ...
.
To catch or not to catch?
Only catch an exception if you can handle it.
1 void gradeSubmissions() {
2 vector<string> students = loadRoster();
3 for (const string &s : students) {
4 try { /* Open files, grade submission, email student */ }
5 catch (const FileError &e) {
6 cout << "Can't grade: " << s << endl;
7 }
8 }
9 }
Note: In this context, just logging an error message and moving on is reasonable.
Don't catch an exception if you don't know how to handle it and still "do your job" successfully.
1 vector<string> loadRoster() {
2 try {
3 csvstream csvin("280roster.csv"); // ctor may throw
4 // Use the stream to load the roster...
5 }
6 catch (const csvstream_exception &e) {
7 cout << e.what() << endl;
8 }
9 }
Note: If the csvstream fails, we can't do our job (load/return a vector). Instead, we should NOT catch the error here and allow the exception to propagate out of the function.