Exception Handling

Unhandled exceptions

An exception is an unexpected incident that stops the normal program execution. Ex: Accessing an out of range element in vector results in an exception. A program that does not handle an exception ends execution.

Here is an example program:
#include <iostream>
#include <vector>
#include <iomanip>
using namespace std;

int main() {
    int packageWeight;
    double shippingCost;
    vector<double> shippingCostPerLb = {5.00, 6.00, 7.00, 8.50, 10.00, 11.50, 13.00, 15.00, 17.00, 20.00};
    cout << "Enter package weight: ";
    cin >> packageWeight;

    shippingCost = shippingCostPerLb.at(packageWeight - 1);

    cout << "Shipping cost: $" << fixed << setprecision(2);
    cout << shippingCost << endl;

    return 0;

}
Exercise:
Given the program above, which line of code can cause an exception?
Answer: shippingCost = shippingCostPerLb.at(packageWeight - 1);.

An exception can occur on this line, as the value of packageWeight determines the index in shippingCostPerLb that is accessed. As shippingCostPerLb has 10 elements, any value of packageWeight less than 1 or greater than 10 will exceed shippingCostPerLb's range, causing an out_of_range exception.

Catching exceptions

To avoid having a program end when an exception occurs, a program can use try and catch blocks to handle the exception during program execution.

  1. A try block surrounds normal code, which is exited immediately if a statement within the try block throws an exception.
  2. A catch block catches an exception thrown in a preceding try block. If the thrown exception's type matches the catch block's parameter type, the code within the catch block executes. A catch block is called an exception handler.
Let's add try and catch blocks to the above program:
#include <iostream>
#include <vector>
#include <iomanip>
using namespace std;

int main() {
    int packageWeight;
    double shippingCost;
    vector<double> shippingCostPerLb = {5.00, 6.00, 7.00, 8.50, 10.00, 11.50, 13.00, 15.00, 17.00, 20.00};
    cout << "Enter package weight: ";
    
    try {
        cin >> packageWeight;
        shippingCost = shippingCostPerLb.at(packageWeight - 1);
    } catch (out_of_range& excpt) {
        cout << "Error: Invalid weight" << end;
    }

    cout << "Shipping cost: $" << fixed << setprecision(2);
    cout << shippingCost << endl;

    return 0;

}
Exercise:
Given the program above, what type of exception is caught?
Answer: out_of_range.

As the catch block has a parameter of type out_of_range, the catch block will catch any out_of_range exception thrown by the code within the try block.

Handling exceptions

A program may be able to resolve some exceptions. The previous example only printed an error message and an invalid shipping cost. Instead, the program can handle the exception and then get user input again until a valid input is provided, as shown in the below example.
#include <iostream>
#include <vector>
#include <iomanip>
using namespace std;

int main() {
   int packageWeight;
   double shippingCost;
   vector<double> shippingCostPerLb = {5.00, 6.00, 7.00, 8.50, 10.00, 11.50, 13.00, 15.00, 17.50, 20.00};
   bool needInput = true;

   cout << "Enter package weight: ";

   while(needInput) {
      try {
         cin >> packageWeight;
         shippingCost = shippingCostPerLb.at(packageWeight - 1);     
         needInput = false;
      }
      catch (out_of_range& excpt) {
         cout << "Error: Invalid weight" << endl;
      }
   }

   cout << "Shipping cost: $" << fixed << setprecision(2);
   cout << shippingCost << endl;
   
   return 0;
}

Common exception types

Exception TypeCause of exception
runtime_errorErrors that occur and are detected at runtime. runtime_error is a base class for other exceptions.
system_errorErrors originating from the underlying operating system or other low-level system components. system_error's typically have an associated error code.
invalid_argumentErrors due to invalid user inputs or invalid inputs to program components (e.g., function arguments).
out_of_rangeErrors due to accessing elements outside of a supported range (e.g., vector indices).
ios_base::failureErrors due to failures in reading or writing input/output streams.

Throwing exceptions

Using throw statements

A program can throw user-defined exceptions by using a throw statement. A throw statement can throw an exception, like runtime_error, which causes execution to jump immediately to the end of a try block. Ex: The statement throw runtime_error("Invalid date"); creates and throws an exception with the message "Invalid date".

In the following code, if the mixRatio is NaN, the throw statement executes and throws the exception. The catch block catches the exception and outputs the exception message via exception's what() function.
#include <iostream>
#include <cmath>
using namespace std;

int main() {
   double gasVolume;
   double oilVolume;
   double mixRatio = 0.0;

   try {
        cin >> gasVolume;
        cin >> oilVolume;

        mixRatio = gasVolume / oilVolume;

        if (isnan(mixRatio)) {
            throw runtime_error("mixRatio is NaN!")
        }
   } catch (runtime_error& excpt) {
        cout << excpt.what() << end;
   }
   return 0;
}

Exercise:
What is the catch block that catches the exception thrown by the statement throw invalid_argument("Value not greater than 0");?
Answer:
catch (invalid_argument& excpt) {
   // ...
}
.

The thrown exception's type is invalid_argument, which matches the catch block's parameter type. So, the catch block can catch and handle the exception.

Using exceptions to separate error checking from normal code

A programmer can detect errors and throw exceptions to keep error-checking code separate from normal code, and to reduce redundant error checks. The program below computes the density of an object by taking the ratio of mass and volume inputs. If either input is negative, the program throws an exception to handle the error.
#include <iostream>
#include <cmath>
using namespace std;

int main() {
   double massVal = 0;   // Object mass (kg)
   double volumeVal = 0; // Object volume (m^3)
   double densityCalc;   // Resulting density
   
   try {
      cin >> massVal;

      // Error checking, greater than zero mass
      if (massVal <= 0.0) {
         throw runtime_error("Invalid mass");
      }

      cin >> volumeVal;

      // Error checking, greater than zero volume
      if (volumeVal <= 0.0) {
         throw runtime_error("Invalid volume");
      }

      densityCalc = massVal / volumeVal;

      cout << "Density: " << densityCalc;
   }
   catch (runtime_error& excpt) {
      // Prints the error message passed by the throw statement
      cout << excpt.what() << endl;
   }
   
   return 0;
}

Exercise:
What does the program above output for each input sequence?
  1. 1.0 10.0
  2. 5.0 0.0
  3. -5.0 -2.0
Answer:
  1. Density: 0.1
  2. Invalid volume
  3. Invalid mass

Multiple exception handlers

Code within a try block may throw different types of exceptions. The example below uses multiple exception handlers to catch two different types of exceptions.
#include <iostream>
#include <cmath>
using namespace std;

int main() {
   double inputTempValue;
   char inputTempUnit;
   double convertedTempK; 
   
   try {
      cin >> inputTempValue;
      cin >> inputTempUnit;

      // check for valid input temperature unit
      if (inputTempUnit != 'C' && inputTempUnit != 'F') {
         throw invalid_argument("Temperature unit not C or F");
      }

      if (inputTempUnit == 'C') {
         convertedTempK = inputTempValue + 273.15;
      } else {
         convertedTempK = (inputTempValue + 459.67) / 1.8;
      }

      if (convertedTempK < 0.0) {
         throw runtime_error("Temperature less than 0 K");
      }

      cout << inputTempValue << " " << inputTempUnit;
      cout << " = " << convertedTempK << " K" << endl;
   }
   catch (invalid_argument& excpt) {
      cout << "Error: " << excpt.what();
   }
   catch (runtime_error& excpt) {
      cout << "Error: " << excpt.what();
   }
   
   return 0;
}

Handling exceptions thrown by other functions

A function can throw an exception when called by another function. The calling function can handle exceptions thrown by the called function. The program below calls the convertTemp() function within a try block, which allows the calling function to handle exceptions thrown by convertTemp().
#include <iostream>
#include <cmath>
using namespace std;


double convertTemp(char inputTempUnit, double inputTempValue) {
    double convertedTempK;

    if (inputTempUnit != 'C' && inputTempUnit != 'F') {
        throw invalid_argument("Temperature unit not C or F");
    }
    if (inputTempUnit == 'C') {
         convertedTempK = inputTempValue + 273.15;
    } else {
         convertedTempK = (inputTempValue + 459.67) / 1.8;
    }
    return convertedTempK;
}

int main() {
   double inputTempValue;
   char inputTempUnit;
   double convertedTempK; 
   
   try {
      cin >> inputTempValue;
      cin >> inputTempUnit;

      convertedTempK = convertTemp(inputTempUnit, inputTempValue);
      
      if (convertedTempK < 0.0) {
         throw runtime_error("Temperature less than 0 K");
      }

      cout << inputTempValue << " " << inputTempUnit;
      cout << " = " << convertedTempK << " K" << endl;
   }
   catch (invalid_argument& excpt) {
      cout << "Error: " << excpt.what();
   }
   catch (runtime_error& excpt) {
      cout << "Error: " << excpt.what();
   }
   
   return 0;
}
Exercise:
The following program simulates a vending machine panel. The program reads an item code consisting of a letter representing the row of the item, followed by an integer representing the column of the item. The vending machine accepts item codes with letters A, B, or C, and an integer in the range 1-8. The program then outputs a dispensing message. Fill in the code to throw and handle two exceptions:
  1. If the user enters an invalid letter, then the program throws an exception of type invalid_argument, outputs "Exiting program: Row must be A, B, or C", and exits.
  2. If the user enters a number that is out of range, then the program throws an exception of type runtime_error, outputs "Error: Column must be 1-8", and tries again.
  3. Ex: If the input is B2, then the output is: Dispensing item: B2
    #include <iostream>
    #include <cmath>
    using namespace std;
    
    int main() {
       char row;
       int column;
       bool askForInput = true;
    
       while (askForInput) {
          try {
             cin >> row;
             cin >> column;
    
             if (row != 'A' && row != 'B' && row != 'C') {
                // TODO
             }
             if (column < 1 || column > 8) {
                // TODO
             }
    
             cout << "Dispensing item: " << row << column << endl;
             askForInput = false;
          }
          // TODO
       }
    
       return 0;
    }
    

    Answer:
    #include <iostream>
    #include <cmath>
    using namespace std;
    
    int main() {
       char row;
       int column;
       bool askForInput = true;
    
       while (askForInput) {
          try {
             cin >> row;
             cin >> column;
    
             if (row != 'A' && row != 'B' && row != 'C') {
                throw invalid_argument("Row must be A, B, or C");
             }
             if (column < 1 || column > 8) {
                throw runtime_error("Column must be 1-8");
             }
    
             cout << "Dispensing item: " << row << column << endl;
             askForInput = false;
          }
          catch (invalid_argument& excpt) {
             cout << "Exiting program: " << excpt.what() << endl;
             askForInput = false;
          }
          catch (runtime_error& excpt) {
             cout << "Error: " << excpt.what() << endl;
          }
       }
    
       return 0;
    }
    

Exercise

Student info not found

Given a program that searches for a student's ID or name in a text file, complete the FindID() and FindName() functions. Then, insert a try/catch statement in main() to catch any exceptions thrown by FindID() or FindName(), and output the exception message. Each line in the text file contains a name and ID separated by a space.

Function FindID() has two parameters: a student's name (string) and the text file's contents (ifstream). The function FindID() returns the ID associated with the student's name if the name is in the file, otherwise the function throws a runtime_error with the message "Student ID not found for studentName", where studentName is the name of the student.

Function FindName() has two parameters: a student's ID (string) and the text file's contents (ifstream). The function FindName() returns the name associated with the student's ID if the ID is in the file, otherwise the function throws a runtime_error with the message "Student name not found for studentID", where studentID is the ID of the student.

The main program takes three inputs from a user: the name of a text file (string), the search option for finding the ID or name of a student (int), and the ID or name of a student (string). If the search option is 0, FindID() is invoked with the student's name as an argument. If the search option is 1, FindName() is invoked with the student's ID as an argument. The main program outputs the search result or the caught exception message.

Ex: If the input of the program is:

roster.txt 0 Reagan

and the contents of roster.txt are:

Reagan rebradshaw835
Ryley rbarber894
Peyton pstott885
Tyrese tmayo945
Caius ccharlton329

the output of the program is:

rebradshaw835

Ex: If the input of the program is:

roster.txt 0 Mcauley

the program outputs an exception message:

Student ID not found for Mcauley

Ex: If the input of the program is:

roster.txt 1 rebradshaw835

the output of the program is:

Reagan

Ex: If the input of the program is:

roster.txt 1 mpreston272

the program outputs an exception message:

Student name not found for mpreston272

Answer:
#include <iostream>
#include <string>
#include <stdexcept>
#include <fstream>
using namespace std;


string FindID(string name, ifstream &infoFS) {
   string nextName;
   string nextID;

   infoFS >> nextName;
   infoFS >> nextID;
   while (!infoFS.fail()) {
      if (nextName == name) {
         return nextID;
      }
      else {
         infoFS >> nextName >> nextID;
      }
   }

   throw runtime_error("Student ID not found for " + name);
}

string FindName(string ID, ifstream &infoFS) {
   string nextName;
   string nextID;

   infoFS >> nextName;
   infoFS >> nextID;
   while (!infoFS.fail()) {
      if (nextID == ID) {
         return nextName;
      }
      else {
         infoFS >> nextName >> nextID;
      }
   }

   throw runtime_error("Student name not found for " + ID);
}

int main() {

   int userChoice;
   string studentName;
   string studentID;
   
   string studentInfoFileName;
   ifstream studentInfoFS;
   
   // Read the text file name from user
   cin >> studentInfoFileName;
   
   // Open the text file
   studentInfoFS.open(studentInfoFileName);
   
   // Read search option from user. 0: FindID(), 1: FindName()
   cin >> userChoice;

   try {
      if (userChoice == 0) {
         cin >> studentName;
         studentID = FindID(studentName, studentInfoFS);
         cout << studentID << endl;
      }
      else {
         cin >> studentID;
         studentName = FindName(studentID, studentInfoFS);
         cout << studentName << endl;
      }
   }
   catch (runtime_error &excpt) {
      cout << excpt.what() << endl;
   }

   studentInfoFS.close();
   return 0;
}
Answers will be saved to this Google doc.