Exceptions
An exception carries a failure from the place that detects it to the place that can handle it. throw starts it, catch stops it, and everything in between is unwound: local objects are destroyed and the stack frames disappear.
The cost is paid only when an exception is thrown, which is why it is the right tool for the rare case and the wrong one for a lookup that fails every other call. This lesson throws, catches, watches an object die during unwinding, and rethrows to an outer handler.
An exception object is copied when it is thrown, and its type is the static type of the throw expression: throwing a reference throws a copy of the object. That is why the caught type is the type that was thrown, not the type of the variable the throw named.
Throwing by value and catching by reference is the rule; throwing a pointer is the exception nobody wants, because the handler then has to know whether to delete it.
throw and catch
A try block contains the code that may fail, and the catch clauses after it say which exceptions they handle. The program throws a std::runtime_error and catches it by const reference, which is the standard way: no copy of the exception object.
The what() member returns the message the exception was built with. It is the reason almost every exception in a program derives from std::exception, which declares it.
If no catch matches, the program terminates. That is the last section of this lesson, and it is why an exception should be caught somewhere on every path a program can take.
#include <iostream>
#include <stdexcept>
int main()
{
try {
throw std::runtime_error{"something failed"};
} catch (const std::exception& error) {
std::cout << "caught: " << error.what() << '\n';
}
}
g++ -std=c++23 -Wall -Wextra throw.cpp -o throw_demo
./throw_demo
caught: something failed
Unwinding runs destructors
When an exception leaves a function, every local object in it is destroyed before the handler runs. That is stack unwinding, and it is why RAII from lesson 027 makes exception-safe code possible.
The program's fail() creates a tracker, throws and never reaches its end. The tracker's destructor prints during the unwind, before the catch in main prints the message.
The order is the reverse of construction, as with any scope exit. A resource held by an object is released on the way out, whether the function returns or throws.
#include <iostream>
#include <stdexcept>
class tracker
{
public:
~tracker()
{
std::cout << "destroyed during unwinding\n";
}
};
void fail()
{
tracker local;
throw std::runtime_error{"boom"};
}
int main()
{
try {
fail();
} catch (const std::exception& error) {
std::cout << error.what() << '\n';
}
}
g++ -std=c++23 -Wall -Wextra unwind.cpp -o unwind
./unwind
destroyed during unwinding
boom
Several catch clauses
The clauses are tried in order and the first match wins. A catch of const std::exception& matches every standard exception, so the more specific handlers come first.
The program throws a std::out_of_range, which is a std::logic_error and a std::exception. The handler for std::exception catches it and prints what(), and the second throw is caught by the int handler.
Catching by value copies the exception and slices a derived one; catching by const reference keeps the real type. The reference also avoids a copy that may itself throw.
#include <iostream>
#include <stdexcept>
#include <string>
int main()
{
try {
throw std::out_of_range{"index 3 is past the end"};
} catch (const std::out_of_range& error) {
std::cout << "range: " << error.what() << '\n';
} catch (const std::exception& error) {
std::cout << "other: " << error.what() << '\n';
}
try {
throw 42;
} catch (const std::string&) {
std::cout << "string\n";
} catch (int value) {
std::cout << "int " << value << '\n';
}
}
g++ -std=c++23 -Wall -Wextra catch_ref.cpp -o catch_ref
./catch_ref
range: index 3 is past the end
int 42
Rethrow
A handler that cannot fully deal with the failure can add context and throw the same exception again with throw;. The bare throw rethrows the current exception without copying it.
The program catches, prints a note and rethrows; the outer handler prints the original message. The exception object is the same one, which is why the message survives both handlers.
Rethrowing is the tool for a layer that logs or translates errors. Translating to another type is a different call: throw some_other_exception{} inside the handler.
#include <iostream>
#include <stdexcept>
void inner()
{
throw std::runtime_error{"disk full"};
}
int main()
{
try {
try {
inner();
} catch (const std::exception& error) {
std::cout << "inner saw: " << error.what() << '\n';
throw;
}
} catch (const std::exception& error) {
std::cout << "outer saw: " << error.what() << '\n';
}
}
g++ -std=c++23 -Wall -Wextra rethrow.cpp -o rethrow
./rethrow
inner saw: disk full
outer saw: disk full
An int has no what()
An exception can be any type, including int, but only the types derived from std::exception have what(). Calling it on an int is a member access on a non-class type.
The program catches an int and calls what(). The error says the member is requested from an int, which has none, and the fix is to throw a standard exception or to print the int with <<.
A program that throws plain ints loses the message and the hierarchy. Throwing std::runtime_error or a type derived from it keeps both, which lesson 079 builds on.
#include <iostream>
int main()
{
try {
throw 42;
} catch (int value) {
std::cout << value.what() << '\n';
}
}
g++ -std=c++23 -Wall -Wextra int_exception.cpp -o int_exception
int_exception.cpp: In function 'int main()':
int_exception.cpp:8:28: error: request for member 'what' in 'value', which is of non-class type 'int'
8 | std::cout << value.what() << '\n';
| ^~~~
Exercises
- Write a function that throws std::invalid_argument for a negative size and catch it in main.
- Add a second local object to fail() and check the destruction order in the output.
- Wrap a call in a try block that logs the message and rethrows, and catch the exception one level up.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
throwdo?Answer
It starts an exception from the current point
The thrown object propagates until a matching catch is found.
-
What happens to local objects while an exception propagates?
Answer
They are destroyed by stack unwinding
The destructors run for every frame the exception leaves.
-
Why catch by reference?
Answer
It avoids a copy and keeps the dynamic type
A reference to the base exception reaches the derived object's what().
-
What does
throw;inside a catch do?Answer
It rethrows the current exception
The bare rethrow keeps the original object and its type.
-
What does this program print?
#include <iostream> #include <stdexcept> #include <string> int main() { try { throw std::runtime_error{"something failed"}; } catch (const std::exception& error) { std::cout << "caught: " << error.what(); } }Answer
caught: something failed
The catch reads the message through what() and adds its own prefix.
-
What does this program print?
#include <iostream> #include <stdexcept> struct guard { ~guard() { std::cout << "destroyed during unwinding\n"; } }; void work() { guard g; throw std::runtime_error{"boom"}; } int main() { try { work(); } catch (const std::exception& error) { std::cout << error.what(); } }Answer
destroyed during unwinding / boom
The guard's destructor runs as the exception leaves work, and main prints the message after.
-
What does this program print?
#include <iostream> #include <stdexcept> int main() { try { throw std::out_of_range{"index 3 is past the end"}; } catch (const std::out_of_range& error) { std::cout << "range: " << error.what() << '\n'; } catch (const std::exception&) { std::cout << "other\n"; } try { throw 42; } catch (int value) { std::cout << "int " << value; } }Answer
range: ... then int 42
The first exception matches its specific clause, and the int matches its own catch.
-
What does this program print?
#include <iostream> #include <stdexcept> void inner() { try { throw std::runtime_error{"disk full"}; } catch (const std::exception& error) { std::cout << "inner saw: " << error.what() << '\n'; throw; } } int main() { try { inner(); } catch (const std::exception& error) { std::cout << "outer saw: " << error.what(); } }Answer
inner saw: disk full / outer saw: disk full
The rethrow sends the same exception to the outer handler.
-
What does the compiler report for this line?
#include <iostream> int main() { try { throw 42; } catch (int value) { std::cout << value.what(); } }Answer
request for member 'what' in 'value', which is of non-class type 'int'
An int has no what(), so the program would have to print the number itself.
-
When does an exception cost the program time?
Answer
When it is thrown, not when the try block is entered
The zero-cost model pays at the throw, which is why exceptions are cheap on the normal path.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev