Exception Types

Gain a thorough understanding of exception types, including how to throw and catch both standard library and custom exceptions in your code
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
3D Character Concept Art
Ryan McCombe
Ryan McCombe
Updated

Rather than throwing simple primitive objects like integers and strings, our exceptions get much more powerful when we throw types that are specifically designed to represent errors.

This gives us the ability to define variables and class functions on our error objects, giving our throw blocks many more options to inspect and react to the exception.

Standard Library Exception Types

The C++ standard library comes with a hierarchy of exception classes. The standard library exception types inherit from std::exception, but we’ll typically be using more specific derived types. Derived types 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

These give us a standardized type of object to throw within our functions. 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 the same way as throwing any other object. The standard library types (aside from the basic std::exception type) 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. Objects that inherit from std::exception have a what() method that allows us to access a description of the exception:

#include <iostream>
#include <stdexcept>

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

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 cannot be constructed with a message. It does implement the what() method, but this is only so it can be overridden by child classes, enabling polymorphism. The base what() method will just return a generic string, such as "std::exception"

Note, that this does not apply to the Microsoft implementation of std::exception. There, the basic std::exception does support an error string.

However, we should strive to write code that is broadly portable across different environments. Also, we should generally not be creating basic std::exception objects anyway - we should typically be throwing objects with more specific types.

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, and by const reference if preferred.

This is important as, just like functions arguments, exceptions will be passed to catch blocks by value otherwise. This has twe undesirable effects.

  1. It causes the usual performance degradation involved in copying an object unnecessarily.
  2. When dealing with inheritance, as it can result in data loss. If we throw a derived type like std::invalid_argument but catch it by the value of a base type like std::exception, we lose any class variables that are not part of the base std::exception type.

This data loss is sometimes referred to as slicing - any variables from the more specific types are sliced off when we copy the object to a base type that doesn’t contain those variables.

We have an example of this below. Note the two try-catch blocks are almost identical - the only difference is the first block is catching by reference, whilst the second is catching by value:

#include <iostream>
#include <stdexcept>

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

int main() {
  try {
    Divide(2, 0);
  } catch (std::exception& e) {
    std::cout << "Error 1: " << e.what();
  }

  try {
    Divide(2, 0);
  } catch (std::exception e) {
    std::cout << "\nError 2: " << e.what();
  }
}

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.

In most compilers, this will include the custom error message we set, so the second catch block cannot determine the nature of the error:

Error 1: Cannot divide by zero
Error 2: std::exception

When Multiple Catchers Match

Given the nature of inheritance, multiple catch statements may 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 std::logic_error exception. However, because std::logic_error is a subclass of std::exception, what we are throwing is also a std::exception.

As a result, the first catch statement can handle the error. 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, std::logic_error exceptions will always be handled by the first catch block. 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 Custom Exception Types

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. Below, we introduce a user-defined AuthenticationError type:

#include <iostream>

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

  std::string Message{"A user failed to log in"};
  std::string Email;
  std::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 custom exception types, we will want to maintain a hierarchy, similar to std::exception. This allows us to create more generic catch statements, by having them catch the more generic base types.

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

When doing that, we should adopt the convention of making our error message available through the what() function, which we can override:

#include <string>

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

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

  std::string Email;
  std::string Password;
};

The noexcept Specifier

The previous code example annotates our what() function with the noexcept specifier.

This specifier tells the compiler (and other developers) that this function will not leak any exceptions to the caller.

We cover the noexcept specifier, its implications, and when we should use it in more detail in the next lesson.

In the previous example, we needed to apply noexcept simply because the function we’re overriding - std::exception::what() - is marked noexcept. We need to maintain that guarantee for an override to be valid.

Now, our exceptions can be caught by specific catch statements, tailored to handle our specific type:

#include <iostream>

class AuthenticationError{/*...*/} int main() { try { throw AuthenticationError{ "test@email.com", "wrong"}; } catch (AuthenticationError& e) { std::cout << "AuthenticationError Handler:\n" << e.what() << "\n EMail: " << e.Email << "\n Password: " << e.Password; } }
AuthenticationError handler:
A user failed to log in
  EMail: test@email.com
  Password: wrong

But they can also be caught by higher-level, more general handlers:

#include <iostream>

class AuthenticationError{/*...*/} int main() { try { throw AuthenticationError{ "test@email.com", "wrong"}; } catch (std::exception& e) { std::cout << "std::exception Handler:\n" << e.what(); } }
std::exception Handler:
A user failed to log in

Summary

In this lesson, we introduced the notion of exception types, We covered std::exception and its hierarchy, alongside the creation and handling of custom exception types.

Key Takeaways:

  • Understanding the standard library has an exception hierarchy, based on std::exception and with derived types like std::logic_error and std::runtime_error.
  • Techniques for throwing and catching exceptions, emphasizing the importance of catching by reference to avoid object slicing.
  • The concept of inheritance in exception handling, ensuring that more specific exceptions are caught before more general ones.
  • Best practices for creating and using custom exception types, integrating them into the std::exception hierarchy, and overriding the what() method.
  • The application of the noexcept specifier in exception handling, indicating functions that won’t throw exceptions.

Was this lesson useful?

Next Lesson

std::terminate and the noexcept specifier

Learn how exceptions interact with the call stack, and how to use terminate, set_terminate, and abort.
3D Concept Art of a Dragon
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

Exception Types

Gain a thorough understanding of exception types, including how to throw and catch both standard library and custom exceptions in your code

A computer programmer
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
Exceptions and Error Handling
A computer programmer
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:

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

std::terminate and the noexcept specifier

Learn how exceptions interact with the call stack, and how to use terminate, set_terminate, and abort.
3D Concept Art of a Dragon
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved