Exceptions

Exceptions separate error handling from normal code. A function that can't do its job throws an exception; the program jumps to the nearest matching catch, destroying local objects on the way (so RAII cleans everything up). They're C++'s standard way of reporting errors that the immediate caller can't reasonably handle.

throw, try and catch

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

double safe_divide(double a, double b) {
    if (b == 0) throw std::invalid_argument("division by zero");
    return a / b;
}

int main() {
    try {
        std::cout << safe_divide(10, 4) << '\n';
        std::cout << safe_divide(1, 0) << '\n';      // throws
        std::cout << "never printed\n";
    } catch (const std::invalid_argument& e) {
        std::cout << "error: " << e.what() << '\n';
    }
    std::cout << "program continues\n";
}
Output
2.5
error: division by zero
program continues

Catch exceptions by const reference. what() returns the message given when the exception was created.

Standard exception types

The standard library's exceptions form a hierarchy rooted at std::exception. Catching a base class catches all its derived types too:

ExceptionTypical cause
std::invalid_argumentA bad argument value (std::stoi("abc"))
std::out_of_range.at() with a bad index or key
std::length_errorA container asked to grow beyond its maximum
std::runtime_errorErrors only detectable at run time (files, network…)
std::bad_allocMemory allocation failed
std::logic_errorBase of errors in program logic
C++
#include <iostream>
#include <map>
#include <stdexcept>
#include <string>
#include <vector>

int main() {
    std::vector<int> v{1, 2, 3};
    std::map<std::string, int> ages{{"Ana", 31}};

    for (int test = 0; test < 3; ++test) {
        try {
            if (test == 0) v.at(10) = 1;
            if (test == 1) std::cout << ages.at("Zoe") << '\n';
            if (test == 2) std::cout << std::stoi("twelve") << '\n';
        } catch (const std::out_of_range& e) {
            std::cout << "out of range\n";
        } catch (const std::exception& e) {          // anything else from the standard library
            std::cout << "other error: " << e.what() << '\n';
        }
    }
}
Output
out of range
out of range
other error: stoi: no conversion

Handlers are tried in order, so put more specific types first. A catch (...) catches absolutely anything, but gives no information about what happened.

Your own exception types

Derive from a standard exception to add meaning — and extra data — to your errors:

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

class ParseError : public std::runtime_error {
public:
    ParseError(const std::string& message, int line)
        : std::runtime_error(message + " on line " + std::to_string(line)), line_(line) {}
    int line() const { return line_; }
private:
    int line_;
};

void parse_config() {
    throw ParseError("missing '='", 14);
}

int main() {
    try {
        parse_config();
    } catch (const ParseError& e) {
        std::cout << "config error: " << e.what() << " (line " << e.line() << ")\n";
    }
}
Output
config error: missing '=' on line 14 (line 14)

Stack unwinding

When an exception propagates out of functions, every local object is destroyed in reverse order. That's why RAII objects never leak, even on errors:

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

struct Step {
    std::string name;
    explicit Step(std::string n) : name(std::move(n)) { std::cout << "begin " << name << '\n'; }
    ~Step() { std::cout << "end " << name << '\n'; }
};

void inner() {
    Step s{"inner"};
    throw std::runtime_error("something failed");
}

void outer() {
    Step s{"outer"};
    inner();
}

int main() {
    try {
        outer();
    } catch (const std::exception& e) {
        std::cout << "caught: " << e.what() << '\n';
    }
}
Output
begin outer
begin inner
end inner
end outer
caught: something failed

Rethrowing

A handler can do some work (such as logging) and pass the exception on with a bare throw;, which preserves its original type:

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

void load() {
    try {
        throw std::runtime_error("disk not ready");
    } catch (const std::exception& e) {
        std::cout << "log: " << e.what() << '\n';
        throw;                                   // let the caller decide
    }
}

int main() {
    try {
        load();
    } catch (const std::runtime_error& e) {
        std::cout << "main: giving up (" << e.what() << ")\n";
    }
}
Output
log: disk not ready
main: giving up (disk not ready)

noexcept

Mark functions that never throw noexcept. It documents the guarantee and enables optimisations — notably, containers move elements during reallocation only if the move constructor is noexcept. If a noexcept function does throw, the program terminates. Destructors are noexcept by default, and must never throw.

Uncaught exceptions

An exception that escapes main ends the program with std::terminate, without unwinding guarantees:

C++
#include <stdexcept>

int main() {
    throw std::runtime_error("nobody caught me");
}
Output
libc++abi: terminating due to uncaught exception of type std::runtime_error: nobody caught me

The message printed depends on the standard library; the program exits abnormally.

When to use exceptions

  • Use them for errors that the immediate caller usually can't fix: a missing file, invalid data, a failed connection, violated preconditions in constructors.
  • Don't use them for expected outcomes in normal control flow — "not found" in a search is better returned as std::optional (lesson 43).
  • Some fields (games, embedded, real-time) disable exceptions for predictability; they use error codes or std::expected instead.

Try it yourself

Write a function that parses an age from text and throws a custom exception for non-numbers and impossible ages.

Try it yourself · main.cpp
Edit the code, copy it into main.cpp, then compile and run it with c++ -std=c++20 -Wall main.cpp -o main && ./main. How to set up a C++ compiler
Output
age 34
invalid: "abc" is not a number
invalid: 200 is not a realistic age
invalid: unexpected characters in "12kg"