Exceptions

C++ · Practical · 14 min

Exceptions are the standard library's error mechanism, so you need to handle them even if your own code reports errors differently.

throw signals an error that the current function can't handle. Control leaves the function and unwinds the stack until a matching catch is found; along the way every fully constructed local object is destroyed. That's why RAII matters: destructors free memory, close files and release locks during unwinding, so you don't need cleanup code at each exit.

Throw objects derived from std::exception (header <stdexcept> has std::runtime_error, std::invalid_argument, std::out_of_range and others), and catch by const reference: catch (const std::exception& e). Catching by value slices derived exceptions and copies them. Handlers are tried in order, so put specific types first. catch (...) catches anything; inside a handler, throw; rethrows the current exception.

If no handler is found, std::terminate ends the program.

noexcept declares that a function won't throw; if it does, the program terminates. Destructors are noexcept by default. Mark move constructors and move assignment noexcept too, because containers like std::vector only move elements during reallocation when the move can't throw.

Think about exception-safety guarantees:

  • Basic: after an exception nothing leaks and objects are still valid.
  • Strong: the operation either succeeds or has no effect (commit-or-rollback, often via copy-and-swap).
  • Nothrow: the operation never fails.

On mainstream compilers exceptions are "zero-cost" when nothing is thrown, but throwing is slow (microseconds). Use them for exceptional failures such as a missing config file or out of memory, not for normal control flow like "key not found". Some codebases (games, embedded) disable them, which is why the Advanced lessons cover std::optional and std::expected.

Example

#include <iostream>
#include <stdexcept>
#include <string>

struct Guard {
    ~Guard() { std::cout << "cleanup\n"; }      // runs during stack unwinding
};

int parse_port(const std::string& s) {
    Guard g;
    int port = std::stoi(s);                     // throws std::invalid_argument on "abc"
    if (port < 1 || port > 65535)
        throw std::out_of_range("port out of range: " + s);
    return port;
}

int main() {
    for (std::string input : {"8080", "99999", "abc"}) {
        try {
            int port = parse_port(input);
            std::cout << "port " << port << '\n';
        } catch (const std::out_of_range& e) {   // most specific first
            std::cout << "range error: " << e.what() << '\n';
        } catch (const std::exception& e) {      // base of all standard exceptions
            std::cout << "bad input: " << e.what() << '\n';
        }
    }
}   // each input prints "cleanup" before its result or error

Remember

  • Throw std::exception-derived types; catch by const&, most specific first
  • Stack unwinding runs destructors, so RAII makes code exception-safe
  • Mark move operations noexcept; use exceptions for exceptional failures only

Watch out for

  • Catching std::exception before std::out_of_range, so the specific handler is never reached.

Check yourself

  1. Why should a move constructor be marked noexcept?
  2. A function throws while a std::vector, a std::unique_ptr and an open std::ofstream are local variables. What happens to them?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: en.cppreference.com/w/cpp/language/exceptions