C++ Exception Types

In this lesson, we will learn about the std::exception types that come with the C++ standard library. We will also learn how to create and use our own custom exception types.
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

vb5.jpg
Ryan McCombe
Ryan McCombe
Posted

The C++ standard library comes with its own hierarchy of exception classes. These give us a standardized type of object to throw within our exceptions.

All exceptions in the standard library inherit from std::exception, but it is typical to use a more specific subclass. Options include classes like std::logic_error and std::runtime_error.

These have yet more specific subclasses - for example, std::logic_error has a subclass called std::invalid_argument

How we classify our exceptions generally doesn’t matter, but it’s helpful to be consistent.

Throwing and Catching std::exception objects

The standard library exceptions are available after including stdexcept:

#include <stdexcept>

Throwing exception objects works in exactly the same way as throwing any other. These classes have a constructor that we can pass a string to, explaining the issue:

#include <iostream>
#include <stdexcept>

int Divide(int x, int y) {
  if (y == 0) {
    throw std::invalid_argument {
      "Cannot divide by zero"
    };
  }
  return x/y;
}

Similarly, we catch them like any other. std::exceptions have a what method that allows us to access the description of the exception:

int main() {
  try {
    Divide(4,0);
  } catch (std::invalid_argument& e) {
    std::cout << "Invalid Argument: "
              << e.what();
  }
}
Invalid Argument: Cannot divide by zero

Custom Messages with the base std::exception

The base std::exception class does not have the ability to be constructed with a message. It does implement the what method, but it will always return the message "std::exception"

In general, we should not be creating basic std::exception objects - we should typically be throwing more specific subclasses.

Catching By Reference

In the above examples, note that we catch the errors by reference, denoted by the inclusion of the & character:

catch (std::invalid_argument& e)

By convention, we should generally be catching errors by reference.

This is important as, just like functions, catch blocks will, by default, catch by value. This has undesirable effects, particularly when dealing with inheritance. If we throw a derived type like std::invalid_argument but catch it by the value of a base type like std::exception we can lose data.

Below is an example. Note the two blocks of code are almost identical - the only difference is the first block is catching by reference, whilst the second is catching by value:

try {
  throw std::invalid_argument {
    "Useful Error Message"
  };
} catch (std::exception& e) {
  std::cout << e.what() << std::endl;
}

try {
  throw std::invalid_argument {
    "Useful Error Message"
  };
} catch (std::exception e) {
  std::cout << e.what()  << std::endl;
}
Useful Error Message
std::exception

Because the second code is copying a std::invalid_argument into the simpler std::exception, it has lost all the data that is not part of the std::exception type. This includes the custom error message. This is referred to as slicing, and it is usually a mistake we want to avoid.

When Multiple Catchers Match

Given the nature of inheritance, it is possible that multiple catch statements match the specific type of exception thrown. When this happens, the first valid catch statement is what is activated. Consider this example:

int main() {
  try {
    throw std::logic_error("Oops!");
  } catch (std::exception& e) {
    std::cout << "Exception: " << e.what();
  } catch (std::logic_error& e) {
    std::cout << "Logic Error: " << e.what();
  }
}

Here, we are throwing a logic_error exception. However, because logic_error is a subclass of std::exception, what we are throwing is also a std::exception.

As a result, the catch statement on line 4 is used. Our output is:

Exception: std::exception

In this case, the catcher for std::logic_error is a useless piece of code. It could never be reached because, under the rules of inheritance, if an object is a std::logic_error it is also a std::exception, so it will always be caught by the earlier catch statement.

Our tools may warn us as such:

main.cpp:7:12: warning: exception of type 'std::logic_error'
will be caught by earlier handler

Because of this “first catcher wins” behavior, we should ensure the more specific catchers are first:

int main() {
  try {
    throw std::logic_error("Oops!");
  } catch (std::logic_error& e) {
    std::cout << "Logic Error: " << e.what();
  } catch (std::exception& e) {
    std::cout << "Exception:" << e.what();
  }
}
Logic Error: Oops!

Creating Our Own Exception Classes

We are not restricted to just throwing the std::exception types - we can throw and catch any object type. This allows us to add fields to our errors that are specific to the needs of our application:

#include <iostream>

using std::string;

class AuthenticationError {
 public:
  AuthenticationError(string Email, string Password)
      : Email(Email), Password(Password) {}

  string Message{"A user failed to log in"};
  string Email;
  string Password;
};

int main() {
  try {
    throw AuthenticationError {
      "test@email.com", "something-wrong"
    };
  } catch (AuthenticationError& e) {
    std::cout << e.Message
              << "\n  E-Mail: " << e.Email
              << "\n  Password: " << e.Password;
  }
}
A user failed to log in
  E-Mail: test@email.com
  Password: something-wrong

Typically, when creating our own errors, we will want to maintain a hierarchy of errors, similar to std::exception. This allows us to create more generic catch statements, by having them catch the more generic types.

It’s also common for teams just to use the standard library’s implementation for this purpose, and to insert their own custom errors into the std::exception hierarchy.

When doing that, we should adopt the convention of making our error message available through the what() function, in the same way std::exception does:

class AuthenticationError : public std::exception {
 public:
  AuthenticationError(string Email, string Password)
      : Email(Email), Password(Password) {}

  const char* what() const noexcept override {
    return "A user failed to log in";
  };

  string Email;
  string Password;
};

int main() {
  try {
    throw AuthenticationError{"test@email.com", "wrong"};
  } catch (AuthenticationError& e) {
    std::cout << "Caught by a specific handler:\n";
    std::cout << e.what()
              << "\n  EMail: " << e.Email
              << "\n  Password: " << e.Password;
  }

  try {
    throw AuthenticationError{"test@email.com", "wrong"};
  } catch (std::exception& e) {
    std::cout << "\n\nAlso caught by generic handler:\n";
    std::cout << e.what();
  }
}
Caught by specific handler:
A user failed to log in
  EMail: test@email.com
  Password: wrong

Also caught by generic handler:
A user failed to log in

Was this lesson useful?

Ryan McCombe
Ryan McCombe
Posted
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Exceptions and Error Handling
7a.jpg
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access!

This course includes:

  • 106 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Exceptions and the Call Stack

Learn how exceptions interact with the call stack, and how to use terminate, set_terminate, and abort.
123.jpg
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved