Skip to content
Theory · Exceptions · lesson 3 of 7

Standard exceptions

The standard library throws exceptions too. The at method of a vector checks the index and, if there is no such element, throws a std::out_of_range exception:

C++
#include <iostream>
#include <stdexcept>
#include <vector>

int main() {
    std::vector<int> v{1, 2, 3};
    try {
        std::cout << v.at(5) << '\n';
    } catch (const std::out_of_range&) {
        std::cout << "no element 5\n";  // no element 5
    }
}

In the containers theme, at stopped the program for a missing key. Any exception that nobody catches ends the same way.

The std::exception hierarchy

The standard exception classes derive from std::exception in the <exception> header. Its virtual method what() returns a description of the error as a const char* string. The classes below are declared in the <stdexcept> header:

  • std::logic_error—an error in the program's logic: a condition the caller could have checked in advance is broken;
  • std::invalid_argument—an invalid argument, derived from std::logic_error;
  • std::out_of_range—an index or a key outside the valid bounds, also derived from std::logic_error;
  • std::runtime_error—an error that can be detected only while the program runs.

The constructor of each of these classes takes a message text, and what() returns exactly that text:

C++
int parse_digit(char c) {
    if (c < '0' || c > '9') {
        throw std::invalid_argument("not a digit");
    }
    return c - '0';
}

A handler for std::exception catches any standard exception, and the virtual what() is called on the object that was thrown:

C++
try {
    std::cout << parse_digit('x') << '\n';
} catch (const std::exception& e) {
    std::cout << e.what() << '\n';  // not a digit
}

Your own exception class is also declared as a derived class of one of these, usually std::runtime_error. Then a handler for std::exception catches it too, and what() returns the text passed to the std::runtime_error constructor.

Throw values, catch references

A throw expression creates a separate object from its operand—the exception object. It lives at least until the handler that caught it finishes. So you throw the object itself, not a pointer to it: a pointer to a local variable dangles after stack unwinding.

An exception is caught by const reference: const std::exception& e denotes the exception object itself, with no copy. It can also be caught by value, catch (std::exception e), but then e is a new copy of the type in parentheses. For a derived class this is object slicing: the copy is a plain std::exception. Calling what() on it runs std::exception::what, not the thrown class's version.

Common mistake

The exception is created with new:

C++
try {
    throw new std::runtime_error("disk full");  // ✗
} catch (const std::exception& e) {
    std::cout << e.what() << '\n';
}

The compiler does not warn, but the thrown value is a std::runtime_error* pointer, while the handler expects a reference to std::exception. The pointer does not fit it, there is no other handler, and std::terminate is called. Throw the object itself: throw std::runtime_error("disk full");.