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
#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"; }
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:
| Exception | Typical cause |
|---|---|
std::invalid_argument | A bad argument value (std::stoi("abc")) |
std::out_of_range | .at() with a bad index or key |
std::length_error | A container asked to grow beyond its maximum |
std::runtime_error | Errors only detectable at run time (files, network…) |
std::bad_alloc | Memory allocation failed |
std::logic_error | Base of errors in program logic |
#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'; } } }
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:
#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"; } }
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:
#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'; } }
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:
#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"; } }
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:
#include <stdexcept> int main() { throw std::runtime_error("nobody caught me"); }
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::expectedinstead.
Try it yourself
Write a function that parses an age from text and throws a custom exception for non-numbers and impossible ages.
age 34 invalid: "abc" is not a number invalid: 200 is not a realistic age invalid: unexpected characters in "12kg"