Skip to content
Theory · Exceptions · lesson 2 of 9

Order of handlers

In the previous lesson one handler followed the try block. There can be several, and each one waits for an exception of its own type.

Back to the building with floors. On the floor that set up a try, the nets hang one below another, and each net carries a sign with an exception type. The ball falls into the first net whose sign fits it and never reaches the lower ones. The sign "input error" also fits the ball "empty input": empty input is a special case of an input error.

The first one that fits

Handlers are checked from top to bottom, and the first one that fits runs. Unlike overloading, nothing looks for the best match here.

A handler fits if its parentheses name the exception's type or its public base class, by value or by reference. So a handler for a base class also catches exceptions of derived classes:

C++
#include <iostream>

struct InputError {};
struct EmptyInput : public InputError {};

int main() {
    try {
        throw EmptyInput{};
    } catch (const EmptyInput&) {
        std::cout << "empty input\n";  // empty input
    } catch (const InputError&) {
        std::cout << "input error\n";
    }
}

Let's walk through what happens here:

  1. EmptyInput is declared as derived from InputError.
  2. The try block throws an EmptyInput.
  3. The const EmptyInput& handler is checked first: the type matches, and the program prints empty input.
  4. The InputError handler is never reached. It would catch a different input error, such as an InputError itself.

Hence the rule: write handlers for derived classes first, then for base classes.

Catching everything: catch (...)

A catch (...) handler, with three dots in parentheses, catches an exception of any type. It is the widest net, so it hangs last. If you put it above another handler, the program does not compile: Clang reports catch-all handler must come last.

You cannot find out what exactly catch (...) caught: the exception has no name in it. Such a handler usually prints a general message or passes the exception on.

Passing it on: throw;

Inside a handler, the expression throw; with no operand throws the same exception further, to an outer try. This way a handler can print a message and pass the error up:

C++
try {
    try {
        throw EmptyInput{};
    } catch (const InputError&) {
        std::cout << "log ";
        throw;
    }
} catch (const EmptyInput&) {
    std::cout << "empty input\n";  // log empty input
}

Step by step:

  1. The inner handler catches the EmptyInput through a reference to InputError and prints log .
  2. throw; sends on the same exception object, not a copy of it.
  3. The outer EmptyInput handler catches it, because the object's real type is kept, and prints empty input.

This is where the nets analogy is inexact: a real net does not toss the ball on, while a handler can throw the exception further.

When there is no handler

If no handler fits anywhere, std::terminate is called and the program ends. The stack is not guaranteed to unwind then: the destructors of local objects may not run at all.

Common mistake

A base-class handler comes before a derived-class handler:

C++
struct InputError {};
struct EmptyInput : public InputError {};

void report() {
    try {
        throw EmptyInput{};
    } catch (const InputError&) {
        std::cout << "input error\n";  // input error
    } catch (const EmptyInput&) {  // ✗
        std::cout << "empty input\n";
    }
}
TEXT
warning: exception of type 'const EmptyInput &' will be caught by earlier handler [-Wexceptions]

The program builds, but the second handler never runs: the first handler catches EmptyInput before it. This is an interview question: "what does a program with these handlers print?" Swap the handlers, and empty input gets printed.

Key points

  • Handlers are checked from top to bottom, and the first one that fits runs, not the best one.
  • A base-class handler also catches derived exceptions, so derived handlers go first.
  • catch (...) catches an exception of any type and comes last.
  • throw; inside a handler throws the same exception object further; if no handler exists anywhere, std::terminate is called.

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