throw, try and catch
What you'll learn:
- throw an exception with
throwand catch it withtryandcatch; - explain which functions end and which destructors run on the way from
throwtocatch; - put handlers in the right order and catch exceptions by reference;
- explain why a destructor must not throw.

Picture a building where every function call is a separate floor. main works on the bottom floor. When a function calls another one, a new floor appears above it, and the work moves up there. When the function returns its result, its floor disappears, and the work goes on one floor below.
What if trouble strikes on the top floor and nobody there can deal with it? You could send a made-up number down, but downstairs it would be taken for a real one. It is more honest to throw an alarm ball down. It flies past the floors that do not know what to do with it: they switch off the lights and leave. The ball is caught by a net on the floor that prepared for this kind of trouble in advance.
First let's see what is wrong with a made-up number. The function divide cannot divide by zero, but its return type obliges it to return some int. Say it returns -1 for a zero divisor:
#include <iostream>
int divide(int a, int b) {
if (b == 0) {
return -1;
}
return a / b;
}
int main() {
std::cout << divide(-7, 7) << ' ' << divide(1, 0) << '\n'; // -1 -1
}The caller cannot tell the error from a real quotient: -7 / 7 is also -1. Whatever number you pick, it may turn out to be a genuine result of a division.
Throwing an exception
Instead of a special value, the function can throw an exception—an object that reports an error bypassing return. This is our alarm ball. A throw expression creates it and throws it. You can throw an int or a string too, but usually a separate class is declared for the error:
struct DivideByZero {};
int divide(int a, int b) {
if (b == 0) {
throw DivideByZero{};
}
return a / b;
}After throw, the function returns no value, and the line return a / b does not run. The exception leaves divide, then the function that called it, and so on—until it reaches code that is ready to handle it.
Catching an exception
Code that may throw an exception goes into a try block: this is how a floor puts up its net. The block is followed by handlers—catch blocks with an exception type in parentheses:
int main() {
try {
std::cout << divide(7, 2) << '\n'; // 3
std::cout << divide(1, 0) << '\n';
std::cout << "done\n";
} catch (const DivideByZero&) {
std::cout << "division by zero\n"; // division by zero
}
}Let's walk through what happens here:
divide(7, 2)returns3, and the program prints it.divide(1, 0)throws an exception, so the rest of thetryblock is skipped anddoneis not printed.- The
DivideByZerohandler runs and printsdivision by zero. - After the handler, the code that follows the last
catchruns.
Exceptions are switched off in the course exercises: code with throw or try does not compile there. That is why this theme has only theory and quizzes.
Stack unwinding
Back to the building. While the ball flies down, every floor between throw and try closes: the function ends, and its local objects are destroyed in reverse order. This is stack unwinding: destructors of local objects run even when an exception leaves the function.
Let's check it with an object whose destructor prints the number of its floor:
#include <iostream>
struct Lamp {
int floor;
~Lamp() { std::cout << "off " << floor << '\n'; }
};
void upper() { Lamp lamp{2}; throw 42; }
void lower() { Lamp lamp{1}; upper(); std::cout << "lower end\n"; }
int main() {
try {
lower();
} catch (int code) {
std::cout << "caught " << code << '\n'; // caught 42
}
}The program prints three lines. Let's go through them in order:
maincallslower, which creates the floor1lamp and callsupper, anduppercreates the floor2lamp and throws42.- The top floor closes first: the destructor of the lamp in
upperprintsoff 2. - Then
lowercloses:off 1is printed, and the linelower endnever runs. - The exception reaches the
tryinmain, and the handler printscaught 42.
Step through the same program: watch how the call frames are removed and in what order the destructors print.
1#include <iostream>2struct Lamp {3 int floor;4 ~Lamp() {5 std::cout << "off " << floor << "\n";6 }7};8 9void upper() {10 Lamp lamp{2};11 throw 42;12}13void lower() {14 Lamp lamp{1};15 upper();16 std::cout << "lower end\n";17}18 19int main() {20 try {21 lower();22 } catch (int code) {23 std::cout << "caught " << code << "\n";24 }25}
main enters the try block and calls lower
This is where the floors analogy is inexact: a closed floor does not wait for the ball to be caught and never opens again. A function that an exception has passed through does not resume its work.
Interviewers like to ask this: what happens to local objects while an exception is in flight? The answer: their destructors run, from the last one created to the first, until the exception is caught.
Key points
throwthrows an exception—an object that reports an error bypassingreturn.- Code that may throw goes into
try, andcatchhandlers catch the exception. - After
throw, the rest of the function and the rest of thetryblock do not run. - During stack unwinding, the functions between
throwandcatchend, and the destructors of their locals run in reverse order.
Subscribe to take the course
The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.