C++ course menu
C++ Exceptions: try, catch, throw and Exception Safety
Learn C++ exception handling: throwing and catching exceptions, the standard exception hierarchy, catching by const reference, multiple catch blocks, rethrowing, custom exception classes, stack unwinding with RAII, noexcept, and when to use exceptions.
On this page
Some errors cannot be handled where they happen. A function that parses a configuration file may find a bad value, but only the caller knows whether to show a message, use a default, or stop. Exceptions let the code that detects a problem throw it, and code further up the call chain catch it.
throw, try and catch#
#include <iostream>
#include <stdexcept>
double divide(double a, double b) {
if (b == 0) {
throw std::invalid_argument("division by zero");
}
return a / b;
}
int main() {
try {
std::cout << divide(10, 4) << '\n';
std::cout << divide(1, 0) << '\n';
std::cout << "this line is skipped\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 continuesthrowcreates an exception object and leaves the function immediately.- C++ looks for the nearest enclosing
tryblock with a matchingcatch. - The rest of the
tryblock is skipped, thecatchblock runs, and execution continues after it. - If nothing catches the exception, the program terminates.
The standard exceptions#
The standard library throws, and you can reuse, a hierarchy of exception classes from <stdexcept>. All derive from std::exception, which has a what() message:
invalid_argument, out_of_range and length_error derive from std::logic_error; others such as overflow_error derive from std::runtime_error.
Several catch blocks#
catch blocks are tried in order, so put specific types first and general ones last:
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
void process(const std::string& input) {
std::vector<int> data = {10, 20, 30};
int index = std::stoi(input); // may throw invalid_argument
std::cout << data.at(index) << '\n'; // may throw out_of_range
}
int main() {
for (std::string input : {"1", "7", "x"}) {
try {
process(input);
} catch (const std::out_of_range&) {
std::cout << input << ": no such position\n";
} catch (const std::exception& e) {
std::cout << input << ": invalid input\n";
}
}
}20
7: no such position
x: invalid inputcatch (const std::exception&) catches anything derived from std::exception. catch (...) catches everything, including non-standard types, but gives no information about the error.
Always catch by const reference. Catching by value copies the exception and can slice a derived exception into its base type.
Custom exceptions#
Derive from a standard exception to add your own types. Callers can then catch your specific error, or catch std::exception for all errors:
#include <iostream>
#include <stdexcept>
#include <string>
class InsufficientFunds : public std::runtime_error {
public:
InsufficientFunds(double balance, double requested)
: std::runtime_error("cannot withdraw " + std::to_string(static_cast<int>(requested)) +
", balance is " + std::to_string(static_cast<int>(balance))),
shortfall_(requested - balance) {}
double shortfall() const { return shortfall_; }
private:
double shortfall_;
};
class Account {
public:
explicit Account(double balance) : balance_(balance) {}
void withdraw(double amount) {
if (amount > balance_) throw InsufficientFunds(balance_, amount);
balance_ -= amount;
}
private:
double balance_;
};
int main() {
Account acc(500);
try {
acc.withdraw(200);
acc.withdraw(800);
} catch (const InsufficientFunds& e) {
std::cout << e.what() << " (short by " << e.shortfall() << ")\n";
}
}cannot withdraw 800, balance is 300 (short by 500)Stack unwinding and RAII#
When an exception leaves a function, every local object in that function is destroyed in reverse order, running its destructor. This is called stack unwinding, and it is why RAII is so important: resources owned by objects are released even when an error interrupts the code:
#include <iostream>
#include <stdexcept>
struct Guard {
const char* name;
~Guard() { std::cout << "release " << name << '\n'; }
};
void step2() {
Guard g{"lock"};
throw std::runtime_error("disk full");
}
void step1() {
Guard g{"file"};
step2();
std::cout << "never printed\n";
}
int main() {
try {
step1();
} catch (const std::exception& e) {
std::cout << "caught: " << e.what() << '\n';
}
}release lock
release file
caught: disk fullWith raw new/delete or manual close() calls, the exception would skip the cleanup and leak. With smart pointers, containers and file streams, cleanup is automatic.
Rethrowing#
A catch block can do some work, such as logging, and then pass the same exception on with throw;:
#include <iostream>
#include <stdexcept>
void load() {
try {
throw std::runtime_error("network down");
} catch (const std::exception& e) {
std::cout << "log: " << e.what() << '\n';
throw; // rethrow the original exception
}
}
int main() {
try {
load();
} catch (const std::exception& e) {
std::cout << "main handles: " << e.what() << '\n';
}
}log: network down
main handles: network downWrite throw;, not throw e;: the second form throws a copy, possibly sliced to the base type.
noexcept#
Marking a function noexcept promises that it never throws. The compiler and the standard library can then optimize (for example, a vector moves elements whose move constructor is noexcept instead of copying them). If a noexcept function does throw, the program terminates. Destructors are noexcept by default; never let an exception escape a destructor.
When to use exceptions#
- Use them for exceptional conditions that the immediate code cannot handle: invalid input from far away, a missing file, a broken network.
- Do not use them for normal control flow, such as ending a loop or "not found" results that happen all the time. Return a
bool,std::optional(see Modern C++) or an error code instead. - Throw by value, catch by
constreference.
Common mistakes#
Practice#
Practice exercise
Write int parse_age(const std::string& text) that converts the text with std::stoi and throws std::out_of_range("age must be 0-130") if the result is outside 0–130. Call it for "25", "abc" and "200", printing either the age or the error message.
Show solution
#include <iostream>
#include <stdexcept>
#include <string>
int parse_age(const std::string& text) {
int age = std::stoi(text); // throws std::invalid_argument for "abc"
if (age < 0 || age > 130) {
throw std::out_of_range("age must be 0-130");
}
return age;
}
int main() {
for (std::string input : {"25", "abc", "200"}) {
try {
int age = parse_age(input);
std::cout << "age " << age << '\n';
} catch (const std::invalid_argument&) {
std::cout << input << ": not a number\n";
} catch (const std::out_of_range& e) {
std::cout << input << ": " << e.what() << '\n';
}
}
}age 25
abc: not a number
200: age must be 0-130The age is parsed into a variable before printing. Writing std::cout << "age " << parse_age(input) would print age first, because C++17 evaluates the operands of << from left to right, and the exception would arrive halfway through the line.
Key takeaway#
Finished this lesson?