Skip to content
Theory · Exceptions · lesson 1 of 9

throw, try and catch

What you'll learn

What you'll learn:

  • throw an exception with throw and catch it with try and catch;
  • explain which functions end and which destructors run on the way from throw to catch;
  • put handlers in the right order and catch exceptions by reference;
  • explain why a destructor must not throw.
A building in cross-section: the ball throw falls from the top floor past darkened rooms into the net catch on the bottom floor main

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:

C++
#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:

C++
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:

C++
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:

  1. divide(7, 2) returns 3, and the program prints it.
  2. divide(1, 0) throws an exception, so the rest of the try block is skipped and done is not printed.
  3. The DivideByZero handler runs and prints division by zero.
  4. After the handler, the code that follows the last catch runs.

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:

C++
#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:

  1. main calls lower, which creates the floor 1 lamp and calls upper, and upper creates the floor 2 lamp and throws 42.
  2. The top floor closes first: the destructor of the lamp in upper prints off 2.
  3. Then lower closes: off 1 is printed, and the line lower end never runs.
  4. The exception reaches the try in main, and the handler prints caught 42.

Step through the same program: watch how the call frames are removed and in what order the destructors print.

LiveStack unwinding: from throw to catchstep 1 of 10 · frames: 1

main enters the try block and calls lower

Watch which frames are removed and whose destructors print

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

  • throw throws an exception—an object that reports an error bypassing return.
  • Code that may throw goes into try, and catch handlers catch the exception.
  • After throw, the rest of the function and the rest of the try block do not run.
  • During stack unwinding, the functions between throw and catch end, 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.

Sign in another way