Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 13 – Error handling

C++ exceptions: throw, catch and stack unwinding

Throw and catch C++ exceptions the right way, watch stack unwinding run destructors on two compilers, and choose the standard exception class to derive from.

  • Intermediate
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Catch exceptions by const reference and throw them by value
  • Predict which destructors stack unwinding runs, and when it runs none
  • Choose the standard exception class to derive your own exceptions from
  • Predict when std::terminate ends a program instead of a handler

Before you start

What you’ll learn · Before you start

What you will learn

  • Catch exceptions by const reference and throw them by value
  • Predict which destructors stack unwinding runs, and when it runs none
  • Choose the standard exception class to derive your own exceptions from
  • Predict when std::terminate ends a program instead of a handler

Before you start

On this page

read_marks finds an empty line three calls below main, and neither it nor the two functions between it and main can do anything useful about it. C++ reports such a failure with an exception: An object thrown with throw to report that a function cannot do what it was asked. Control leaves the function at once and passes up the chain of callers to the first handler whose type matches the object.: the function creates an object that describes the problem and throws it, and control leaves at once for the nearest try block up the chain of callers that has a matching catch handler. On the way, stack unwinding: What happens while a thrown exception travels to its catch block: every local object constructed since the try block began, and not yet destroyed, is destroyed in reverse order before the handler runs. More in “Destructors and RAII in C++” destroys every local object of every function it leaves, newest first, so RAII: Resource acquisition is initialisation: a class acquires a resource, such as an open file or a locked mutex, in its constructor and releases it in its destructor, so every path out of the object's scope releases it exactly once. More in “Destructors and RAII in C++” objects still close their files and release their locks although no function returned normally. If no handler exists anywhere, the program calls std::terminate and ends.

Exceptions suit failures that the caller cannot sensibly check for at every step: a missing file, malformed input found deep inside a parser, memory that ran out. Below, one throw is traced through three functions with Apple clang 21 and GCC 16, caught four ways, matched against the standard classes, and stopped by noexcept and by builds without exceptions.

A throw leaves every function between it and the handler

A try block marks the code whose exceptions you want to handle, and the exception handler: A catch block after a try block, such as catch (const std::out_of_range& e). The handlers are tried in the order they are written; the first whose type is the thrown type, or a public base class of it, runs, and catch (...) matches any exception. after it say which ones. When an exception reaches the block, its handlers are tried in the order they are written, and the first whose type matches runs: a handler for the same class as the exception, or for a public base class of it. A handler written catch (...) matches any exception at all. After the handler finishes, the program continues below the last handler of that try block; it never goes back to the code that threw.

Tracer prints + when it is created and - when it is destroyed. Three functions each create one and call the next, and the innermost one throws when the line it is given is empty:

Three functions, each with a local object that reports its lifetime

C++ · unwind.cpp

#include <print>
#include <stdexcept>
#include <string_view>
#include "tracer.h"

void read_marks(std::string_view line) {
    Tracer buffer{"read_marks: buffer"};
    if (line.empty()) throw std::invalid_argument("empty line");
    std::println("  read_marks read \"{}\"", line);
}

void load_student(std::string_view line) {
    Tracer record{"load_student: record"};
    read_marks(line);
    std::println("  load_student finished");  // skipped when read_marks throws
}

void import_class(std::string_view line) {
    Tracer log{"import_class: log"};
    load_student(line);
    std::println("  import_class finished");  // skipped too
}

int main(int argc, char*[]) {
    if (argc > 1) {  // ./unwind uncaught: the same throw, and no catch anywhere
        std::println("an empty line, caught nowhere:");
        import_class("");
        return 0;
    }
    std::println("a good line:");
    import_class("Asha 82");
    std::println("an empty line, caught in main:");
    try {
        import_class("");
    } catch (const std::invalid_argument& e) {
        std::println("  main caught: {}", e.what());
    }
}

C header · tracer.h

// Tracer prints a line when it is created and another when it is destroyed, so that stack unwinding becomes visible.
#pragma once
#include <print>
#include <string>
#include <utility>

class Tracer {
public:
    explicit Tracer(std::string name) : name_(std::move(name)) { std::println("  + {}", name_); }
    ~Tracer() { std::println("  - {}", name_); }
    Tracer(const Tracer&) = delete;
    Tracer& operator=(const Tracer&) = delete;

private:
    std::string name_;
};
A good line, a caught throw and an uncaught throw, with both compilers Python · unwind_session.py
from session import run

# The same three calls with both compilers: a good line, a throw that main catches, and a throw nobody catches.
run("""
clang++ -std=c++23 -Wall -Wextra unwind.cpp -o unwind && ./unwind
./unwind uncaught
g++-16 -std=c++23 -Wall -Wextra unwind.cpp -o unwind-gcc && ./unwind-gcc
./unwind-gcc uncaught
""")

Output

$ clang++ -std=c++23 -Wall -Wextra unwind.cpp -o unwind && ./unwind
a good line:
  + import_class: log
  + load_student: record
  + read_marks: buffer
  read_marks read "Asha 82"
  - read_marks: buffer
  load_student finished
  - load_student: record
  import_class finished
  - import_class: log
an empty line, caught in main:
  + import_class: log
  + load_student: record
  + read_marks: buffer
  - read_marks: buffer
  - load_student: record
  - import_class: log
  main caught: empty line
$ ./unwind uncaught
an empty line, caught nowhere:
  + import_class: log
  + load_student: record
  + read_marks: buffer
libc++abi: terminating due to uncaught exception of type std::invalid_argument: empty line
[exit status 134]
$ g++-16 -std=c++23 -Wall -Wextra unwind.cpp -o unwind-gcc && ./unwind-gcc
a good line:
  + import_class: log
  + load_student: record
  + read_marks: buffer
  read_marks read "Asha 82"
  - read_marks: buffer
  load_student finished
  - load_student: record
  import_class finished
  - import_class: log
an empty line, caught in main:
  + import_class: log
  + load_student: record
  + read_marks: buffer
  - read_marks: buffer
  - load_student: record
  - import_class: log
  main caught: empty line
$ ./unwind-gcc uncaught
an empty line, caught nowhere:
  + import_class: log
  + load_student: record
  + read_marks: buffer
terminate called after throwing an instance of 'std::invalid_argument'
  what():  empty line
[exit status 134]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 unwind_session.py

Step through the second run, the empty line that main catches: each row is one call with the Tracer it holds, and the order in which the three objects go is the point.

Interactive · step by step One throw from read_marks to the handler in main Step through the caught run of unwind.cpp. Each row is one call and its Tracer; watch which objects are destroyed, and in which order.
One throw from read_marks to the handler in main, step 10 of 10. Showing main, main's handler (prints main caught: …), import_class (left, last line skipped), log (destroyed, - printed), load_student (left, last line skipped), record (destroyed, - printed), read_marks (left at the throw), buffer (destroyed, - printed). In focus: main's handler.callscallscallsmaintry { import_class("") }main's handlerprints main caught: …import_classleft, last line skippedlogdestroyed, - printedload_studentleft, last line skippedrecorddestroyed, - printedread_marksleft at the throwbufferdestroyed, - printed

The drawing shows the whole scene; its 10 steps are listed below.

Every step as text
  1. The first call. Inside its try block, main calls import_class with an empty line. import_class creates log, a Tracer, which prints + import_class: log.
  2. Two more calls. load_student creates record and calls read_marks, which creates buffer. Each Tracer prints its + line, so three frames now sit above main, each holding one object.
  3. The throw. read_marks finds the line empty and throws std::invalid_argument("empty line"). The print below the throw never runs: control leaves read_marks at once.
  4. The search starts. Phase one, the search: the runtime looks for a handler that matches std::invalid_argument, frame by frame, and changes nothing. read_marks has no try block, so it looks in load_student next.
  5. Still searching. load_student has no try block either, so the search moves on to import_class. All three Tracers still exist: nothing has been destroyed yet.
  6. A handler matches. import_class has no handler, but main's try block has catch (const std::invalid_argument&), which matches the thrown type. Only now, with a handler found, does the cleanup start.
  7. buffer first. Phase two, the cleanup, destroys the newest object first: buffer prints - read_marks: buffer, and the frame of read_marks is gone.
  8. Then record. Next comes record, in load_student's frame. The rest of load_student is skipped, so load_student finished is never printed.
  9. Then log. log goes last, the oldest of the three, and import_class finished is skipped as well. Every object built since the try block began is now destroyed, in reverse order of construction.
  10. The handler runs. Only then does the catch block run: e.what() gives the message, and main prints main caught: empty line. The program goes on below the handler, never back into read_marks.

The first two runs show the normal path and stack unwinding:

  • A good line. Each function finishes, and each Tracer is destroyed as its own function returns, so the - lines come out in the reverse order of the + lines.
  • An empty line, caught in main. read_marks throws a std::invalid_argument. Before the handler in main runs, the three objects are destroyed, newest first: buffer, then record, then log. The two finished lines never print, because the rest of each function was skipped. e.what() returns the message given to the exception’s constructor.

Both compilers print exactly the same lines, because the order is fixed by the standard: as control passes from the throw to the handler, every local object constructed since the try block was entered is destroyed, in the reverse order of construction.

Checkpoint

Check: A throw and the handler that catches it

  1. Question 1 of 2 In the caught run of unwind.cpp, read_marks has just thrown. Which line does the program print next?

    Choose one answer.

    A hint

    The handler is found first; then the objects go, in the reverse order of their + lines.

    Show the answer: Question 1 of 2

    Answer: - read_marks: buffer

    The search phase finds main's handler without changing anything; then the cleanup destroys buffer, record and log, newest first, and only then does main print main caught: empty line.

  2. Question 2 of 2 A catch block in main finishes without throwing. Where does the program continue?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Below the last handler of that try block

    After a handler finishes, the program continues below the last handler of that try block. The functions the exception left are gone, with their locals destroyed.

Without a handler, no destructor runs

The third run threw the same exception with no try block anywhere. Both programs printed the three + lines and then a message from their standard library, but no - line at all: not one destructor ran. Step through it and watch the three Tracer objects on the right:

Interactive · step by step The same throw with no handler anywhere Step through the uncaught run of unwind.cpp. The search finds no handler, so watch the three Tracers on the right.
The same throw with no handler anywhere, step 6 of 6. Showing main, std::terminate (abort: exit status 134), import_class, log (never destroyed), load_student, record (never destroyed), read_marks, buffer (never destroyed). In focus: log, record, buffer, std::terminate.callscallscallsmainno try blockstd::terminateabort: exit status 134import_classcalled with an empty linelognever destroyedload_studentrecordnever destroyedread_marksbuffernever destroyed

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The same calls. ./unwind uncaught makes the same three calls, this time with no try block anywhere. The three Tracers print their + lines, as in the caught run.
  2. The same throw. read_marks throws std::invalid_argument("empty line"), exactly as before.
  3. The search. The search phase looks for a matching handler in read_marks, then in load_student and import_class. It changes nothing on the way.
  4. Nothing in main. main has no try block either. The search has reached the top of the stack without finding a handler.
  5. std::terminate. With no handler, the cleanup phase never starts. std::terminate runs with the stack exactly as it was at the throw, and the standard library prints its message.
  6. No destructor ran. log, record and buffer are never destroyed, so no - line appears. std::terminate calls std::abort, which ends the process with SIGABRT: exit status 134.

The standard leaves this case to the implementation: when no matching handler is found, std::terminate: The function C++ calls when exception handling cannot go on, for example when no handler matches a throw or an exception tries to leave a noexcept function. By default it calls std::abort, which ends the program with SIGABRT. is called, and whether the stack is unwound first is implementation-defined. Both compilers here follow the Itanium C++ ABI, which throws in two phases. A search phase walks up the stack looking for a handler and changes nothing. Only if it finds one does a cleanup phase run the destructors on the way to it. With no handler, the cleanup phase never starts, and std::terminate sees the stack exactly as it was at the throw. The diagram puts the two outcomes side by side:

A throw first searches up the stack for a handler; only if one is found are the locals destroyed on the way to it.throw in read_marksPhase 1, search:look for a matching catch,frame by frame up the stack,changing nothingHandlerfound?Phase 2, cleanup:destroy each frame's locals,newest first, on the wayto the handlerThe catch block runsstd::terminate:no destructor has run,the stack is untouchedyesno

A throw in two phases, as both compilers implement it

Text description of the diagram

The diagram shows what happens after a throw, in the order both compilers on this page implement it (the Itanium C++ ABI).

  1. An exception is thrown in read_marks.
  2. Phase 1, the search: the runtime looks for a matching catch handler, frame by frame up the stack, without changing anything.
  3. A decision: was a handler found?
  4. If yes, phase 2, the cleanup: the local objects of every frame between the throw and the handler are destroyed, newest first, and then the catch block runs.
  5. If no, std::terminate is called at once. No destructor has run and the stack is still exactly as it was at the throw.

That has a practical consequence. A destructor that matters, such as one that writes the last buffered lines of a log, runs after an error only if something catches the error. A handler in main that catches const std::exception&, prints the message and returns a non-zero exit status therefore does two jobs: the user gets a readable message, and every destructor runs. Its cost is that the place of the throw is gone by the time the handler runs, which matters when you want a stack trace of the failure; the lesson on debugging crashes, later in this module, shows how to record the stack before the program ends.

Key idea

Unwinding needs a handler: the search must find a matching catch before the cleanup destroys anything, so an exception that nothing catches runs no destructor at all with these two compilers.

Checkpoint

Check: Unwinding needs a handler

  1. Question 1 of 2 ./unwind uncaught creates three Tracer objects and throws, with no try block anywhere. How many - lines does it print with Apple clang 21 or GCC 16?

    Type a number.

    A hint

    The search phase runs first. What does it find?

    Show the answer: Question 1 of 2

    Answer: 0 lines

    The search phase finds no handler, so the cleanup phase never starts: no destructor runs and no - line is printed. std::terminate prints the library's message and std::abort ends the program with exit status 134.

  2. Question 2 of 2 A Logger writes its buffered lines in its destructor. read_marks throws and nothing catches the exception, so the last lines are lost. Which change makes the Logger's destructor run, with both compilers?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A handler in main that catches const std::exception&, prints e.what() and returns a non-zero status

    Unwinding needs a handler. A catch in main for const std::exception& gives the search something to find, so the cleanup destroys every local object between the throw and main, and the user also gets a readable message.

Catching by value slices the exception, and throw e; does too

A program can throw any copyable type, but a purpose-made class is clearer than a bare int or string, rule E.14 of the C++ Core Guidelines says, because its type names the problem and its members carry the details. ParseError is a std::runtime_error that also knows the line where parsing failed. The same exception is caught here in four ways:

catching.cpp C++ · catching.cpp
#include <print>
#include <stdexcept>
#include <string>

// A purpose-made exception type: a std::runtime_error that also knows where the problem is.
class ParseError : public std::runtime_error {
public:
    ParseError(const std::string& message, int line) : std::runtime_error(message), line_(line) {}
    int line() const noexcept { return line_; }

private:
    int line_;
};

void parse_settings() { throw ParseError("expected '=' after the key", 7); }

int main() {
    try {
        parse_settings();
    } catch (std::exception e) {  // by value: a new std::exception, copied from part of the ParseError
        std::println("by value:     {}", e.what());
    }

    try {
        parse_settings();
    } catch (const std::exception& e) {  // by reference: the object that was thrown
        std::println("by reference: {}", e.what());
    }

    for (bool copy : {false, true}) {
        try {
            try {
                parse_settings();
            } catch (const std::exception& e) {
                if (copy) throw e;  // throws a copy of the std::exception part
                throw;              // rethrows the ParseError itself
            }
        } catch (const ParseError& e) {
            std::println("{:<13} still a ParseError, line {}", copy ? "throw e;" : "throw;", e.line());
        } catch (const std::exception& e) {
            std::println("{:<13} only a std::exception now: {}", copy ? "throw e;" : "throw;", e.what());
        }
    }
}
By value, by reference, throw; and throw e; Python · catching_session.py
from session import run

# Catching by value and by reference, then rethrowing with throw; and throw e;, with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra catching.cpp -o catching && ./catching
g++-16 -std=c++23 -Wall -Wextra catching.cpp -o catching-gcc && ./catching-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra catching.cpp -o catching && ./catching
by value:     std::exception
by reference: expected '=' after the key
throw;        still a ParseError, line 7
throw e;      only a std::exception now: std::exception
$ g++-16 -std=c++23 -Wall -Wextra catching.cpp -o catching-gcc && ./catching-gcc
catching.cpp: In function 'int main()':
catching.cpp:20:29: warning: catching polymorphic type 'class std::exception' by value [-Wcatch-value=]
   20 |     } catch (std::exception e) {  // by value: a new std::exception, copied from part of the ParseError
      |                             ^
by value:     std::exception
by reference: expected '=' after the key
throw;        still a ParseError, line 7
throw e;      only a std::exception now: std::exception

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 catching_session.py

Step through the four parts, with the thrown ParseError on the left and what each handler receives on the right:

Interactive · step by step What a handler receives, by value and by reference Step through the four parts of catching.cpp. The left column is the ParseError that parse_settings throws; the right column is what each handler gets.
What a handler receives, by value and by reference, step 5 of 5. Showing std::exception part, runtime_error part, ParseError part, e in the handler (copied again, as exception), the outer handlers (only std::exception matched). In focus: e in the handler, the outer handlers.copiedstd::exception partvirtual what()runtime_error partexpected '=' after the keyParseError partline_ = 7e in the handlercopied again, as exceptionthe outer handlersonly std::exception matchedthrow e;

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The thrown object. parse_settings throws ParseError("expected '=' after the key", 7). The object has three layers: its std::exception base, the message that std::runtime_error keeps, and ParseError's own line_, 7.
  2. By value. catch (std::exception e) creates a new std::exception and copies only the std::exception part into it. The copy is sliced: e.what() is the base class's own text, so the program prints by value: std::exception.
  3. By const reference. catch (const std::exception& e) binds e to the object that was thrown, all three layers. Its virtual what() is the ParseError's, so the line printed is by reference: expected '=' after the key.
  4. throw;. Inside a handler, throw; with no operand rethrows the exception being handled, the ParseError itself. The outer catch (const ParseError&) matches and reads line 7.
  5. throw e;. throw e; throws a new copy of e's static type, std::exception. The outer ParseError handler no longer matches; the std::exception handler after it catches the copy, and the line is lost: only a std::exception now.

  • By value, catch (std::exception e) creates a new std::exception and copies into it only the std::exception part of the thrown ParseError. The copy is slicing: What happens when an object of a derived class is copied into a variable or a parameter of a base class type: only the base part is copied, the derived members are left behind, and the copy behaves as a plain base object. More in “Virtual functions, override and virtual destructors”: its what() is the base class’s own message, std::exception with both libraries, and the line number is gone. GCC warns about it with -Wcatch-value, which -Wall turns on; Clang compiles it silently.
  • By const reference, the handler sees the object that was thrown, with its real message. A handler for a base class can still reach the derived members with dynamic_cast, or by catching const ParseError& directly.
  • throw; with no operand rethrow: throw; with no operand, written inside a handler: it throws the exception being handled again, the same object with its full type, so an outer handler for the derived class still matches. throw e; throws a copy of e's static type instead. the exception being handled, the ParseError itself, so the outer handler for ParseError still matches.
  • throw e; throws a new copy of e’s static type: The type the compiler sees for an expression, from the declarations alone: through a Shape* the object has the static type Shape, whatever it really is. Calls to ordinary member functions are chosen from it. More in “Virtual functions, override and virtual destructors”, std::exception, so the outer ParseError handler no longer matches and the line number is lost again.

Rule E.15 of the Core Guidelines sums it up: throw by value, catch exceptions from a class hierarchy by reference, usually const. Never write throw new ParseError(…), which throws a pointer that some handler must remember to delete.

Catching by value

catch (std::exception e) turns a thrown ParseError into a plain std::exception: what() says only std::exception, and the line is gone. Write catch (const std::exception& e), and pass the exception on with throw;, never throw e;.

Checkpoint

Check: Catching by reference, rethrowing with throw;

  1. Question 1 of 2 A ParseError is thrown. Which handler can still read its line()?

    Choose one answer.

    A hint

    Which handlers see the object that was thrown, and which see a copy?

    Show the answer: Question 1 of 2

    Answer: catch (const ParseError& e), then e.line()

    Only a handler that refers to the thrown object can reach its derived members: catch (const ParseError& e), or a base-class reference followed by a dynamic_cast. A handler that catches by value copies the base part only.

  2. Question 2 of 2 Inside catch (const std::exception& e), which statement passes the original exception, with its full type, to the next handler up the stack?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: throw;

    throw; rethrows the object being handled, so catching.cpp's outer ParseError handler still matched and read line 7. throw e; threw a copy of the static type, and the line was lost.

Decide by the exception’s type, never by its message

The standard library reports its own failures with a small family of classes, all derived from std::exception, whose virtual what() returns a message:

std::exception is the base of logic_error, runtime_error and a few classes such as bad_alloc, each with its own subclasses.std::exceptionvirtual what()Derived from std::exceptionstd::logic_error:the caller broke a rulestd::runtime_error:found only while runningDirectly from std::exceptioninvalid_argumentdomain_errorlength_errorout_of_rangerange_error,overflow_errorformat_errorsystem_errorfilesystem_error(a system_error)bad_allocbad_castbad_optional_accessbad_variant_access

The standard exception classes most programs meet

Text description of the diagram

The diagram is a family tree of standard exception classes, with std::exception at the top. Its virtual member function what() returns the message.

  1. std::logic_error, for mistakes the caller could have avoided by checking first. Its subclasses: invalid_argument, domain_error, length_error and out_of_range.
  2. std::runtime_error, for failures that are found only while the program runs. Its subclasses: range_error and overflow_error, format_error, and system_error. filesystem_error derives from system_error.
  3. Classes that derive from std::exception directly: bad_alloc, bad_cast, bad_optional_access and bad_variant_access.

Every class shown is in namespace std; filesystem_error is std::filesystem::filesystem_error.

std::logic_error is for mistakes the caller could have avoided by checking first, such as an index past the end. std::runtime_error is for failures that can only be found while the program runs, whatever the caller does, such as a missing file. Here eight library calls are made in turn, and each handler names the class it caught:

standard.cpp C++ · standard.cpp
#include <cstddef>
#include <filesystem>
#include <new>
#include <print>
#include <stdexcept>
#include <string>
#include <vector>

// Runs one call and reports which standard exception came out of it. The handlers go from the most derived
// classes to std::exception, because the first handler that matches wins.
template <typename Call>
void report(const char* what, Call call) {
    try {
        call();
        std::println("{:<30} no exception", what);
    } catch (const std::out_of_range& e) {
        std::println("{:<30} out_of_range: {}", what, e.what());
    } catch (const std::invalid_argument& e) {
        std::println("{:<30} invalid_argument: {}", what, e.what());
    } catch (const std::length_error& e) {
        std::println("{:<30} length_error: {}", what, e.what());
    } catch (const std::runtime_error& e) {
        std::println("{:<30} a runtime_error: {}", what, e.what());
    } catch (const std::bad_alloc& e) {
        std::println("{:<30} bad_alloc: {}", what, e.what());
    } catch (const std::exception& e) {
        std::println("{:<30} another exception: {}", what, e.what());
    }
}

int main() {
    std::vector<int> marks{72, 64, 91};
    report("marks.at(2)", [&] { (void)marks.at(2); });
    report("marks.at(3)", [&] { (void)marks.at(3); });
    report("std::stoi(\"seventy\")", [] { (void)std::stoi("seventy"); });
    report("std::stoi(\"99999999999\")", [] { (void)std::stoi("99999999999"); });
    report("std::string(\"abc\").substr(5)", [] { (void)std::string("abc").substr(5); });
    report("marks.reserve(max_size + 1)", [&] { marks.reserve(marks.max_size() + 1); });
    report("operator new(1 PiB)", [] { ::operator delete(::operator new(std::size_t{1} << 50)); });
    report("file_size(\"missing.txt\")", [] { (void)std::filesystem::file_size("missing.txt"); });
}
What each call throws, and what each library says Python · standard_session.py
from session import run

# Which standard exception each library call throws, and the message each standard library gives it.
run("""
clang++ -std=c++23 -Wall -Wextra standard.cpp -o standard && ./standard
g++-16 -std=c++23 -Wall -Wextra standard.cpp -o standard-gcc && ./standard-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra standard.cpp -o standard && ./standard
marks.at(2)                    no exception
marks.at(3)                    out_of_range: vector
std::stoi("seventy")           invalid_argument: stoi: no conversion
std::stoi("99999999999")       out_of_range: stoi: out of range
std::string("abc").substr(5)   out_of_range: basic_string
marks.reserve(max_size + 1)    length_error: vector
operator new(1 PiB)            bad_alloc: std::bad_alloc
file_size("missing.txt")       a runtime_error: filesystem error: in file_size: No such file or directory ["missing.txt"]
$ g++-16 -std=c++23 -Wall -Wextra standard.cpp -o standard-gcc && ./standard-gcc
marks.at(2)                    no exception
marks.at(3)                    out_of_range: vector::_M_range_check: __n (which is 3) >= this->size() (which is 3)
std::stoi("seventy")           invalid_argument: stoi
std::stoi("99999999999")       out_of_range: stoi
std::string("abc").substr(5)   out_of_range: string::string: __pos (which is 5) > this->size() (which is 3)
marks.reserve(max_size + 1)    length_error: vector::reserve
operator new(1 PiB)            bad_alloc: std::bad_alloc
file_size("missing.txt")       a runtime_error: filesystem error: cannot get file size: No such file or directory [missing.txt]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 standard_session.py

Every failing call threw the same class with both libraries, the class the standard specifies for it: at and substr throw std::out_of_range for a position past the end, std::stoi throws std::invalid_argument when there is no number and std::out_of_range when the number does not fit in an int, reserve throws std::length_error, a failed allocation throws std::bad_alloc, and file_size throws std::filesystem::filesystem_error, which the std::runtime_error handler caught because it derives from std::system_error, a kind of std::runtime_error.

The messages are another matter. For marks.at(3), libc++ says vector, while libstdc++ says vector::_M_range_check: __n (which is 3) >= this->size() (which is 3). The standard specifies the class of each exception, never the text of its message, so code should decide what to do from the type it catches and show what() only to people.

The order of the handlers in report matters too. They go from the most derived classes to std::exception, because the first match wins: a std::logic_error handler placed before std::out_of_range would catch every out-of-range error itself. Rule E.31 of the Core Guidelines asks for exactly this order.

For your own exception types, derive from std::logic_error or std::runtime_error, or from one of their subclasses, and add the members a handler needs, as ParseError adds its line. Callers can then catch your exact type where they can do something specific, and std::exception where they only report.

Text Diff Paste the clang and the GCC output of standard.cpp to see that every class matches and nearly every message differs.

Key idea

The standard fixes the class of every exception it throws, never its message: catch by type, most derived class first, and show what() only to people.

Checkpoint

Check: The standard exception classes

  1. Question 1 of 2 What do both standard libraries throw for marks.at(3) on a vector of three marks?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: std::out_of_range

    at() checks its index and throws std::out_of_range for a position past the end. Both libraries threw that class; only their messages differed.

  2. Question 2 of 2 load() throws std::out_of_range. Which handler runs?

    Read the code, then choose one answer.

    try {
        load();
    } catch (const std::exception& e) {
        std::println("A: {}", e.what());
    } catch (const std::out_of_range& e) {
        std::println("B: {}", e.what());
    }
    Show the answer: Question 2 of 2

    Answer: A, because handlers are tried in order and std::exception is a base of std::out_of_range

    The handlers of a try block are tried in the order they are written, and the first one that matches wins. A handler for a base class matches every derived class, so B can never run. Put handlers for derived classes before handlers for their bases.

An exception that leaves a noexcept function ends the program

noexcept: A specifier that promises a function lets no exception out, as in Buffer(Buffer&&) noexcept. If one tries to leave such a function, std::terminate ends the program. A destructor is noexcept unless it says otherwise. More in “Destructors and RAII in C++” after a function’s parameters promises that no exception will leave the function. The compiler relies on the promise, and the standard backs it with a hard rule: if the search for a handler reaches the outermost block of a non-throwing function, std::terminate is called. checked_div breaks its promise:

noexcept_escape.cpp C++ · noexcept_escape.cpp
#include <print>
#include <stdexcept>

int checked_div(int a, int b) noexcept {  // promises never to throw...
    if (b == 0) throw std::domain_error("division by zero");  // ...and breaks the promise
    return a / b;
}

int main() {
    try {
        std::println("{}", checked_div(10, 2));
        std::println("{}", checked_div(1, 0));
    } catch (const std::exception& e) {
        std::println("caught: {}", e.what());  // never reached
    }
}
Warnings at compile time, std::terminate at run time Python · noexcept_session.py
from session import run

# A throw that leaves a noexcept function: both compilers warn, and both programs end in std::terminate.
run("""
clang++ -std=c++23 -Wall -Wextra noexcept_escape.cpp -o noexcept_escape && ./noexcept_escape
g++-16 -std=c++23 -Wall -Wextra noexcept_escape.cpp -o noexcept_escape-gcc && ./noexcept_escape-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra noexcept_escape.cpp -o noexcept_escape && ./noexcept_escape
noexcept_escape.cpp:5:17: warning: 'checked_div' has a non-throwing exception specification but can still throw [-Wexceptions]
    5 |     if (b == 0) throw std::domain_error("division by zero");  // ...and breaks the promise
      |                 ^
noexcept_escape.cpp:4:5: note: function declared non-throwing here
    4 | int checked_div(int a, int b) noexcept {  // promises never to throw...
      |     ^                         ~~~~~~~~
1 warning generated.
5
libc++abi: terminating due to uncaught exception of type std::domain_error: division by zero
Abort trap: 6
[exit status 134]
$ g++-16 -std=c++23 -Wall -Wextra noexcept_escape.cpp -o noexcept_escape-gcc && ./noexcept_escape-gcc
noexcept_escape.cpp: In function 'int checked_div(int, int)':
noexcept_escape.cpp:5:17: warning: 'throw' will always call 'terminate' [-Wterminate]
    5 |     if (b == 0) throw std::domain_error("division by zero");  // ...and breaks the promise
      |                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
terminate called after throwing an instance of 'std::domain_error'
  what():  division by zero
Abort trap: 6
[exit status 134]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 noexcept_session.py

Step through the run. main has a handler that matches the thrown std::domain_error, so watch whether it ever runs:

Interactive · message by message A noexcept function that throws Step through noexcept_escape.cpp. main has a handler that matches, so watch whether it ever runs.
A noexcept function that throws, step 7 of 7. Messages so far: main to checked_div: checked_div(10, 2); checked_div to main: returns 5; main to checked_div: checked_div(1, 0); checked_div to the C++ runtime: std::terminate.maincatch std::exception&checked_divdeclared noexceptthe C++ runtimeabort: exit status 134checked_div(10, 2)returns 5checked_div(1, 0)std::terminate

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Warned while compiling. Both compilers see the broken promise before the program runs: Clang warns with -Wexceptions and GCC with -Wterminate, both on by default. The program still builds.
  2. checked_div(10, 2). Inside its try block, main calls checked_div(10, 2). The divisor is not 0, so nothing is thrown.
  3. 5. checked_div returns 5, and main prints 5, the first line of the run.
  4. checked_div(1, 0). The second call passes 0 as the divisor.
  5. The throw. b is 0, so checked_div throws std::domain_error("division by zero"), although its noexcept promised that no exception would ever leave it.
  6. The promise wins. The search for a handler reaches the outermost block of a non-throwing function. The standard requires std::terminate there, so main's matching handler is never tried and caught: is never printed.
  7. Exit status 134. Each library prints its own message, then std::abort ends the process with SIGABRT, signal 6: the shell reports exit status 134, which is 128 plus 6.

Both compilers see it coming, with Clang’s -Wexceptions and GCC’s -Wterminate, both on by default. At run time, the first call prints 5, and the second ends the program although main has a handler that matches: the exception never got past checked_div. Each library prints its own message, libc++’s terminating due to uncaught exception of type std::domain_error: division by zero and libstdc++’s terminate called after throwing an instance of 'std::domain_error' with the what() text below it. Both then abort the program: exit status: The number a program hands back when it ends, the value main returns. 0 means success; a shell keeps the last status in $?, and && runs the next command only after a status of 0. More in “How C++ compiles: preprocess, compile and link” 134 is 128 plus 6, the number of SIGABRT.

The same ending awaits an exception that leaves main, as the third run of the unwinding example showed, and one that leaves a destructor, because destructors are noexcept by default. So mark a function noexcept only when it really cannot throw, as moves, swaps and destructors should not.

Checkpoint

Check: noexcept and std::terminate

  1. Question 1 of 2 A function declared noexcept throws, and its caller has a matching catch handler. What happens?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: std::terminate is called, and the caller's handler never runs

    When the search for a handler reaches the outermost block of a function with a non-throwing exception specification, the standard requires std::terminate. Both compilers also warn about such a throw at compile time, with -Wexceptions in Clang and -Wterminate in GCC.

  2. Question 2 of 2 Which of these end the program through std::terminate?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • An exception that leaves main
    • An exception that leaves a function declared noexcept
    • An exception that leaves a destructor

    std::terminate ends the program when exception handling cannot go on: no handler anywhere, or an exception at the edge of a function that promised not to throw, which every destructor does by default.

Without exceptions, errors have to travel as values

Some large code bases build with -fno-exceptions. The LLVM project does, to reduce code and executable size, and Google’s C++ style guide does not use exceptions at all. In such a build, throw and try no longer compile, and a library function that would throw has no way to report its error:

A throw and a call to at(), built with -fno-exceptions

C++ (thrower.cpp) · thrower.cpp

#include <stdexcept>

int checked_percent(int marks) {
    if (marks < 0 || marks > 100) throw std::out_of_range("marks must be 0 to 100");
    return marks;
}

int main() { return checked_percent(72) == 72 ? 0 : 1; }

C++ (bounds.cpp) · bounds.cpp

#include <print>
#include <vector>

int main() {
    std::vector<int> marks{72, 64, 91};
    std::println("{}", marks.at(1));
    std::println("{}", marks.at(3));  // at() checks the index and would throw std::out_of_range
    std::println("not reached");
}
Both compilers with exceptions turned off Python · no_exceptions_session.py
from session import run

# Builds with exceptions turned off: throw no longer compiles, and a library call that would throw ends the program.
run("""
clang++ -std=c++23 -fno-exceptions thrower.cpp -o thrower
g++-16 -std=c++23 -fno-exceptions thrower.cpp -o thrower-gcc
clang++ -std=c++23 -fno-exceptions bounds.cpp -o bounds && ./bounds
g++-16 -std=c++23 -fno-exceptions bounds.cpp -o bounds-gcc && ./bounds-gcc
""")

Output

$ clang++ -std=c++23 -fno-exceptions thrower.cpp -o thrower
thrower.cpp:4:35: error: cannot use 'throw' with exceptions disabled
    4 |     if (marks < 0 || marks > 100) throw std::out_of_range("marks must be 0 to 100");
      |                                   ^
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -fno-exceptions thrower.cpp -o thrower-gcc
thrower.cpp: In function 'int checked_percent(int)':
thrower.cpp:4:83: error: exception handling disabled, use '-fexceptions' to enable
    4 |     if (marks < 0 || marks > 100) throw std::out_of_range("marks must be 0 to 100");
      |                                                                                   ^
[exit status 1]
$ clang++ -std=c++23 -fno-exceptions bounds.cpp -o bounds && ./bounds
64
Abort trap: 6
[exit status 134]
$ g++-16 -std=c++23 -fno-exceptions bounds.cpp -o bounds-gcc && ./bounds-gcc
64
terminate called after throwing an instance of 'std::out_of_range'
  what():  vector::_M_range_check: __n (which is 3) >= this->size() (which is 3)
Abort trap: 6
[exit status 134]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 no_exceptions_session.py

The two toolchains agree about your own throw and disagree about the library’s:

With -fno-exceptions Apple clang 21, libc++ GCC 16, libstdc++
A throw in your code Does not compile Does not compile, other wording
marks.at(3) on 3 marks abort(), nothing printed std::terminate and its message
How bounds ends Exit status 134 Exit status 134

Both compilers reject throw, each with its own wording. The call to at(3) ends both programs, in different ways. libc++’s headers replace the throw with a plain abort(), which prints nothing. libstdc++’s range check lives in its compiled library, which was built with exceptions, so the exception is thrown anyway, finds no handler in code compiled without them, and std::terminate prints the usual message. Either way, code built like this reports its errors as values instead: std::expected and error codes, the subject of a later lesson in this module.

Checkpoint

Check: Builds without exceptions

  1. Question 1 of 2 thrower.cpp contains throw std::out_of_range("marks must be 0 to 100"). What happens when it is built with -fno-exceptions?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Both compilers refuse to compile it

    In a -fno-exceptions build, throw and try no longer compile: Clang said cannot use 'throw' with exceptions disabled, and GCC said exception handling disabled, use '-fexceptions' to enable.

  2. Question 2 of 2 bounds.cpp calls marks.at(3) on three marks and is built with Apple clang 21 and -fno-exceptions. How does it end?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: abort() with nothing printed, exit status 134

    bounds.cpp printed 64 and then ended with exit status 134 on both toolchains: silently with libc++, which calls abort(), and with the usual terminate message with libstdc++.

Practise: a safe_divide that reports division by zero

divide.h declares DivisionByZero, a std::domain_error whose message names the number that could not be divided, and three functions that do not use it yet:

The starter code and its sample tests

C header · divide.h

// Your turn (lesson cpp/exceptions-basics): make safe_divide report a zero divisor with an exception, and make
// class_averages recover from it. The sample tests are in divide_tests.cpp.
#pragma once
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>

// What safe_divide throws. It is a std::domain_error, so a caller can also catch it as a std::logic_error or a
// std::exception, and what() names the dividend: "cannot divide 84 by zero".
class DivisionByZero : public std::domain_error {
public:
    explicit DivisionByZero(int dividend)
        : std::domain_error("cannot divide " + std::to_string(dividend) + " by zero"), dividend_(dividend) {}
    int dividend() const noexcept { return dividend_; }

private:
    int dividend_;
};

// TODO 1: when divisor is 0, throw DivisionByZero{dividend} instead of returning 0.
inline int safe_divide(int dividend, int divisor) {
    if (divisor == 0) return 0;
    return dividend / divisor;
}

struct ClassMarks {
    std::string name;
    int total_marks;
    int students;
};

struct Averages {
    std::vector<std::pair<std::string, int>> averages;  // the class and its average, for every class with students
    std::vector<std::string> skipped;                   // the classes that have no students
};

// TODO 2: a class with no students has no average. Catch DivisionByZero for that one class (by const reference),
// add the class's name to skipped and go on with the next class. Let every other exception reach the caller.
inline Averages class_averages(const std::vector<ClassMarks>& classes) {
    Averages result;
    for (const ClassMarks& c : classes) result.averages.emplace_back(c.name, safe_divide(c.total_marks, c.students));
    return result;
}

// TODO 3: the exit status of a report program: 0 when every class has an average, 2 when any class was skipped.
inline int exit_status(const Averages& result) {
    (void)result;
    return 0;
}

C++ · divide_tests.cpp

// Sample tests for divide.h. Build: clang++ -std=c++23 -Wall -Wextra divide_tests.cpp -o divide_tests
#include <print>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
#include "divide.h"

int passed = 0;
int failed = 0;

void expect(bool ok, const char* what) {
    if (ok) {
        ++passed;
    } else {
        ++failed;
        std::println("FAIL {}", what);
    }
}

int main() {
    expect(safe_divide(84, 4) == 21, "safe_divide(84, 4) is 21");

    bool threw = false;
    std::string message;
    int dividend = 0;
    try {
        (void)safe_divide(7, 0);
    } catch (const DivisionByZero& e) {
        threw = true;
        message = e.what();
        dividend = e.dividend();
    }
    expect(threw, "safe_divide(7, 0) throws DivisionByZero");
    expect(message == "cannot divide 7 by zero" && dividend == 7, "the exception names the dividend 7");

    bool as_logic_error = false;
    try {
        (void)safe_divide(1, 0);
    } catch (const std::logic_error&) {
        as_logic_error = true;
    }
    expect(as_logic_error, "the exception can be caught as a std::logic_error");

    std::vector<ClassMarks> classes{{"9A", 840, 20}, {"9B", 0, 0}, {"9C", 1000, 25}};
    Averages result = class_averages(classes);
    std::vector<std::pair<std::string, int>> want{{"9A", 42}, {"9C", 40}};
    expect(result.averages == want && result.skipped == std::vector<std::string>{"9B"},
           "class_averages skips 9B, which has no students, and keeps 9A and 9C");
    expect(exit_status(result) == 2, "the exit status is 2 when a class was skipped");
    expect(exit_status(class_averages({{"10A", 750, 25}})) == 0, "the exit status is 0 when every class has an average");

    std::println("{} of {} checks passed", passed, passed + failed);
    return failed == 0 ? 0 : 1;
}
The sample tests against the starter code Python · divide_session.py
from session import run

# The sample tests against the starter code, before the TODOs are done.
run("""
clang++ -std=c++23 -Wall -Wextra divide_tests.cpp -o divide_tests && ./divide_tests
g++-16 -std=c++23 -Wall -Wextra divide_tests.cpp -o divide_tests-gcc && ./divide_tests-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra divide_tests.cpp -o divide_tests && ./divide_tests
FAIL safe_divide(7, 0) throws DivisionByZero
FAIL the exception names the dividend 7
FAIL the exception can be caught as a std::logic_error
FAIL class_averages skips 9B, which has no students, and keeps 9A and 9C
FAIL the exit status is 2 when a class was skipped
2 of 7 checks passed
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra divide_tests.cpp -o divide_tests-gcc && ./divide_tests-gcc
FAIL safe_divide(7, 0) throws DivisionByZero
FAIL the exception names the dividend 7
FAIL the exception can be caught as a std::logic_error
FAIL class_averages skips 9B, which has no students, and keeps 9A and 9C
FAIL the exit status is 2 when a class was skipped
2 of 7 checks passed
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 divide_session.py

Make all 7 checks pass with both compilers:

  1. In safe_divide, throw DivisionByZero{dividend}; when divisor is 0. Throw the object itself, by value.
  2. In class_averages, put the call for one class inside a try block in the loop, and catch const DivisionByZero& there: add the class’s name to skipped and let the loop go on. Do not catch std::exception, so that any other error still reaches the caller.
  3. In exit_status, return 2 when skipped is not empty, and 0 otherwise, so that a script that runs the report can tell a partial result from a complete one.

The check that catches std::logic_error passes only because DivisionByZero derives from std::domain_error, which derives from std::logic_error: a handler for a base class matches every class below it.

Finish with the self-check on the six ideas above, from the first matching handler to builds without exceptions. It starts where your answers have been weakest and explains each choice as soon as you make it:

Practice that adapts to you

Practise this lesson

12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 An exception reaches a try block with three handlers. Which of them runs?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: The first one, in the order written, whose type matches

    The handlers of a try block are tried in the order they are written, and the first whose type is the thrown type, or a public base class of it, runs. catch (...) matches anything, so it belongs last.

  2. Question 2 of 12 In the good-line run of unwind.cpp, which line comes right after - read_marks: buffer?

    Choose one answer.

    A hint

    Nothing is thrown in this run. When does each Tracer go?

    Show the answer: Question 2 of 12

    Answer: load_student finished

    Each Tracer is destroyed when its own function returns. After the line for buffer come load_student finished, the line for record, import_class finished and the line for log, so the - lines come out in the reverse order of the + lines.

  3. Question 3 of 12 An exception is thrown three functions deep and no function has a matching handler. With Apple clang 21 or GCC 16, what happens to the local objects of those three functions?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: None of them is destroyed, because std::terminate is called before any unwinding

    The standard leaves it to the implementation whether the stack is unwound when no handler exists. Both compilers follow the Itanium C++ ABI: its search phase finds no handler, so the cleanup phase never starts and std::terminate runs with the stack untouched.

  4. Question 4 of 12 In the uncaught run, why did no destructor run?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: The search phase found no handler, so the cleanup phase that runs destructors never started

    The Itanium C++ ABI, which both compilers follow, searches for a handler before it changes anything. With no handler found, std::terminate runs with every object still alive, so code that must run after an error needs a handler, for example in main.

  5. Question 5 of 12 A ParseError, derived from std::runtime_error and constructed with the message "missing key", is caught by catch (std::exception e). What does e.what() return there?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: The message of a plain std::exception, because e is a sliced copy

    Catching by value copies only the std::exception part of the thrown object into a new std::exception, so its what() is the base class's own message: "std::exception" with both standard libraries in the lesson. Catch by const reference to see the object that was thrown.

  6. Question 6 of 12 catching.cpp rethrows with throw e; from inside catch (const std::exception& e). Its outer handlers are catch (const ParseError&) and then catch (const std::exception&). Which line does the program print?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: throw e; only a std::exception now: std::exception

    Both compilers printed throw e; only a std::exception now: std::exception. The copy had the static type of e, std::exception: the message and the line stayed behind with the original.

  7. Question 7 of 12 Which of these failures fit std::runtime_error better than std::logic_error?

    Choose every answer that is right.

    Show the answer: Question 7 of 12

    Answer:

    • A network peer that closed the connection halfway through a message
    • A configuration file that does not exist

    runtime_error is for failures that can only be found while the program runs, whatever the caller does, such as missing files and broken connections. logic_error is for mistakes the caller could have avoided by checking first, such as arguments the documentation rules out.

  8. Question 8 of 12 A test checks that e.what() of marks.at(3) equals "vector", and it passes with Apple clang 21. What happens with GCC 16?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: It fails, because libstdc++ words the message differently

    Test and decide by the type you catch, std::out_of_range here, and show what() only to people. Every failing call in standard.cpp threw the same class on both libraries, and nearly every message differed.

  9. Question 9 of 12 std::terminate calls std::abort, which raises SIGABRT, signal 6. What exit status does the shell report?

    Type a number.

    Show the answer: Question 9 of 12

    Answer: 134

    A shell reports a process ended by a signal as 128 plus the signal's number: 128 + 6 = 134, the status of every run in this lesson that ended in std::terminate.

  10. Question 10 of 12 Which of these functions should be declared noexcept?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: A swap that only exchanges two pointers

    noexcept is a promise the standard enforces with std::terminate. Give it to functions that really cannot throw, as moves, swaps and destructors should not; never to a function whose job includes reporting failures.

  11. Question 11 of 12 In a code base built with -fno-exceptions, how does a function report that it failed?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: In its return value, for example with std::expected or an error code

    Without exceptions, errors are values: std::expected or error codes, the subject of a later lesson in this module.

  12. Question 12 of 12 Why does GCC 16's libstdc++ still print a terminate message for marks.at(3) in a -fno-exceptions build?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: Its range check is in the compiled library, which was built with exceptions, so it throws and finds no handler

    libc++ replaces the throw with abort() in its headers and prints nothing; libstdc++ throws from its compiled library, and with no handler std::terminate prints its message. Both end with exit status 134.

Interview questions

Fresher · Why do you throw by value and catch by const reference?

Throwing by value, as in throw ParseError{"missing key", 7};, lets the runtime own a copy of the exception for as long as handlers need it, with nothing to delete. Catching by reference then refers to that object itself. Catching by value instead copies only the part the handler names: catch (std::exception e) slices a ParseError down to a plain std::exception, so what() loses its message and the extra members are gone, and GCC warns about it with -Wcatch-value. const adds that handlers rarely need to change the exception. A good answer adds the rethrow rule: throw; passes on the original object, while throw e; throws a sliced copy. Both rules are E.15 of the C++ Core Guidelines.

The follow-ups usually test the two mistakes this answer rules out: throwing a pointer, and reaching the derived members from a base-class handler. Decide both:

Checkpoint

Interview round: two follow-up questions

  1. Question 1 of 2 A code review finds throw new ParseError("missing key", 7);. What is wrong with it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It throws a pointer, so a handler for const std::exception& does not match it, and whichever handler catches it must delete the object

    E.15 of the Core Guidelines: throw by value. throw ParseError{"missing key", 7}; lets the runtime own the exception for as long as handlers need it, and every handler for ParseError or a base class by reference can catch it.

  2. Question 2 of 2 Your handler is catch (const std::exception& e), and for a ParseError you also want to log its line. What do you write?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A catch (const ParseError& e) handler before it, or a dynamic_cast<const ParseError*>(&e) inside it

    A handler for a base class can reach the derived members with dynamic_cast, or the derived class can get its own handler, placed first. Both keep deciding by type rather than by message.

Two design questions of this module start from this lesson: a batch job that dies with exit status 134 and loses the last lines of its log, and the exception classes of a library that several teams will use.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A batch job dies with "terminate called after throwing an instance of 'std::runtime_error'" and exit status 134. Its log, written by a class that flushes buffered lines in its destructor, is missing the last lines. Explain both symptoms and what you would change.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    You are designing the exceptions of a payments client library that several teams will use. Which classes do you define, what do they derive from, what data do they carry, and how should callers in different layers catch them?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • throw sends an exception up the call chain to the first matching handler; the handlers of a try block are tried in order, so put derived classes before their bases.
  • Stack unwinding destroys every local object between the throw and the handler, newest first. With no handler at all, both compilers call std::terminate before any destructor runs.
  • Throw by value and catch by const reference: catching by value slices the exception, and throw e; slices a rethrow, while throw; keeps the original.
  • Derive your exceptions from std::logic_error or std::runtime_error and decide by type, never by parsing what(): the two standard libraries word their messages differently.
  • An exception that leaves a noexcept function, a destructor or main calls std::terminate, which aborts with exit status 134.
  • With -fno-exceptions, throw does not compile and a library call that would throw ends the program, so errors have to travel as values.

Terms in this lesson

exception
An object thrown with throw to report that a function cannot do what it was asked. Control leaves the function at once and passes up the chain of callers to the first handler whose type matches the object.
exception handler
A catch block after a try block, such as catch (const std::out_of_range& e). The handlers are tried in the order they are written; the first whose type is the thrown type, or a public base class of it, runs, and catch (...) matches any exception.
rethrow
throw; with no operand, written inside a handler: it throws the exception being handled again, the same object with its full type, so an outer handler for the derived class still matches. throw e; throws a copy of e's static type instead.
std::terminate
The function C++ calls when exception handling cannot go on, for example when no handler matches a throw or an exception tries to leave a noexcept function. By default it calls std::abort, which ends the program with SIGABRT.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.